diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index d7be790cb30..0395556f8ef 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -22,10 +22,6 @@ export type TypedVariableModel = | OrgVariableModel | DashboardVariableModel; -type VarValue = string | number | boolean | undefined; - -export type VariableMap = Record; - export enum VariableRefresh { never, // removed from the UI onDashboardLoad, diff --git a/packages/grafana-runtime/src/services/templateSrv.ts b/packages/grafana-runtime/src/services/templateSrv.ts index b76a7c55e09..502e9de2f9a 100644 --- a/packages/grafana-runtime/src/services/templateSrv.ts +++ b/packages/grafana-runtime/src/services/templateSrv.ts @@ -1,4 +1,4 @@ -import { ScopedVars, TimeRange, TypedVariableModel, VariableMap } from '@grafana/data'; +import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data'; /** * Via the TemplateSrv consumers get access to all the available template variables @@ -18,11 +18,6 @@ export interface TemplateSrv { */ replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string; - /** - * Return the variables and values only - */ - getAllVariablesInTarget(target: string, scopedVars: ScopedVars, format?: string | Function): VariableMap; - /** * Checks if a target contains template variables. */ diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 8494d0424e0..d529243b4ef 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -1,3 +1,4 @@ +import { property } from 'lodash'; import React from 'react'; import { @@ -23,7 +24,8 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; import { LokiQuery } from '../../../plugins/datasource/loki/types'; -import { getFieldLinksForExplore, getVariableUsageInfo } from '../utils/links'; +import { variableRegex } from '../../variables/utils'; +import { getFieldLinksForExplore } from '../utils/links'; import { SpanLinkFunc, Trace, TraceSpan } from './components'; import { SpanLinks } from './components/types/links'; @@ -190,13 +192,7 @@ function legacyCreateSpanLinkFactory( // Check if all variables are defined and don't show if they aren't. This is usually handled by the // getQueryFor* functions but this is for case of custom query supplied by the user. - if ( - getVariableUsageInfo( - dataLink.internal!.query, - scopedVars, - getTemplateSrv().getAllVariablesInTarget.bind(getTemplateSrv()) - ).allVariablesDefined - ) { + if (dataLinkHasAllVariablesDefined(dataLink.internal!.query, scopedVars)) { const link = mapInternalLinkToExplore({ link: dataLink, internalLink: dataLink.internal!, @@ -580,3 +576,65 @@ function scopedVarsFromSpan(span: TraceSpan): ScopedVars { }, }; } + +type VarValue = string | number | boolean | undefined; + +/** + * This function takes some code from template service replace() function to figure out if all variables are + * interpolated. This is so we don't show links that do not work. This cuts a lots of corners though and that is why + * it's a local function. We sort of don't care about the dashboard template variables for example. Also we only link + * to loki/splunk/elastic, so it should be less probable that user needs part of a query that looks like a variable but + * is actually part of the query language. + * @param query + * @param scopedVars + */ +function dataLinkHasAllVariablesDefined(query: T, scopedVars: ScopedVars): boolean { + const vars = getVariablesMapInTemplate(getStringsFromObject(query), scopedVars); + return Object.values(vars).every((val) => val !== undefined); +} + +function getStringsFromObject(obj: T): string { + let acc = ''; + for (const k of Object.keys(obj)) { + // Honestly not sure how to type this to make TS happy. + // @ts-ignore + if (typeof obj[k] === 'string') { + // @ts-ignore + acc += ' ' + obj[k]; + // @ts-ignore + } else if (typeof obj[k] === 'object' && obj[k] !== null) { + // @ts-ignore + acc += ' ' + getStringsFromObject(obj[k]); + } + } + return acc; +} + +function getVariablesMapInTemplate(target: string, scopedVars: ScopedVars): Record { + const regex = new RegExp(variableRegex); + const values: Record = {}; + + target.replace(regex, (match, var1, var2, fmt2, var3, fieldPath) => { + const variableName = var1 || var2 || var3; + values[variableName] = getVariableValue(variableName, fieldPath, scopedVars); + + // Don't care about the result anyway + return ''; + }); + + return values; +} + +function getVariableValue(variableName: string, fieldPath: string | undefined, scopedVars: ScopedVars): VarValue { + const scopedVar = scopedVars[variableName]; + if (!scopedVar) { + return undefined; + } + + if (fieldPath) { + // @ts-ignore ScopedVars are typed in way that I don't think this is possible to type correctly. + return property(fieldPath)(scopedVar.value); + } + + return scopedVar.value; +} diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index c7a2e3a860f..e537de76d7f 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -16,578 +16,470 @@ import { initTemplateSrv } from '../../../../test/helpers/initTemplateSrv'; import { ContextSrv, setContextSrv } from '../../../core/services/context_srv'; import { setLinkSrv } from '../../panel/panellinks/link_srv'; -import { getFieldLinksForExplore, getVariableUsageInfo } from './links'; - -describe('explore links utils', () => { - describe('getFieldLinksForExplore', () => { - beforeEach(() => { - setTemplateSrv( - initTemplateSrv('key', [ - { type: 'custom', name: 'emptyVar', current: { value: null } }, - { type: 'custom', name: 'num', current: { value: 1 } }, - { type: 'custom', name: 'test', current: { value: 'foo' } }, - ]) - ); - }); +import { getFieldLinksForExplore } from './links'; + +describe('getFieldLinksForExplore', () => { + beforeEach(() => { + setTemplateSrv( + initTemplateSrv('key', [ + { type: 'custom', name: 'emptyVar', current: { value: null } }, + { type: 'custom', name: 'num', current: { value: 1 } }, + { type: 'custom', name: 'test', current: { value: 'foo' } }, + ]) + ); + }); - it('returns correct link model for external link', () => { - const { field, range } = setup({ - title: 'external', - url: 'http://regionalhost', - }); - const links = getFieldLinksForExplore({ - field, - rowIndex: ROW_WITH_TEXT_VALUE.index, - splitOpenFn: jest.fn(), - range, - }); - - expect(links[0].href).toBe('http://regionalhost'); - expect(links[0].title).toBe('external'); + it('returns correct link model for external link', () => { + const { field, range } = setup({ + title: 'external', + url: 'http://regionalhost', + }); + const links = getFieldLinksForExplore({ + field, + rowIndex: ROW_WITH_TEXT_VALUE.index, + splitOpenFn: jest.fn(), + range, }); - it('returns generates title for external link', () => { - const { field, range } = setup({ - title: '', - url: 'http://regionalhost', - }); - const links = getFieldLinksForExplore({ - field, - rowIndex: ROW_WITH_TEXT_VALUE.index, - splitOpenFn: jest.fn(), - range, - }); - - expect(links[0].href).toBe('http://regionalhost'); - expect(links[0].title).toBe('regionalhost'); + expect(links[0].href).toBe('http://regionalhost'); + expect(links[0].title).toBe('external'); + }); + + it('returns generates title for external link', () => { + const { field, range } = setup({ + title: '', + url: 'http://regionalhost', + }); + const links = getFieldLinksForExplore({ + field, + rowIndex: ROW_WITH_TEXT_VALUE.index, + splitOpenFn: jest.fn(), + range, }); - it('returns correct link model for internal link', () => { - const { field, range } = setup({ - title: '', - url: '', - internal: { - query: { query: 'query_1' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - panelsState: { - trace: { - spanId: 'abcdef', - }, - }, - }, - }); - const splitfn = jest.fn(); - const links = getFieldLinksForExplore({ - field, - rowIndex: ROW_WITH_TEXT_VALUE.index, - splitOpenFn: splitfn, - range, - }); - - expect(links[0].href).toBe( - `/explore?left=${encodeURIComponent( - '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1"}],"panelsState":{"trace":{"spanId":"abcdef"}}}' - )}` - ); - expect(links[0].title).toBe('test_ds'); - - if (links[0].onClick) { - links[0].onClick({}); - } - - expect(splitfn).toBeCalledWith({ - datasourceUid: 'uid_1', + expect(links[0].href).toBe('http://regionalhost'); + expect(links[0].title).toBe('regionalhost'); + }); + + it('returns correct link model for internal link', () => { + const { field, range } = setup({ + title: '', + url: '', + internal: { query: { query: 'query_1' }, - range, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', panelsState: { trace: { spanId: 'abcdef', }, }, - }); + }, }); - - it('returns correct link model for external link when user does not have access to explore', () => { - const { field, range } = setup( - { - title: 'external', - url: 'http://regionalhost', + const splitfn = jest.fn(); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, splitOpenFn: splitfn, range }); + + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1"}],"panelsState":{"trace":{"spanId":"abcdef"}}}' + )}` + ); + expect(links[0].title).toBe('test_ds'); + + if (links[0].onClick) { + links[0].onClick({}); + } + + expect(splitfn).toBeCalledWith({ + datasourceUid: 'uid_1', + query: { query: 'query_1' }, + range, + panelsState: { + trace: { + spanId: 'abcdef', }, - false - ); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range }); - - expect(links[0].href).toBe('http://regionalhost'); - expect(links[0].title).toBe('external'); + }, }); + }); - it('returns no internal links if when user does not have access to explore', () => { - const { field, range } = setup( - { - title: '', - url: '', - internal: { - query: { query: 'query_1' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - }, - }, - false - ); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range }); - expect(links).toHaveLength(0); - }); + it('returns correct link model for external link when user does not have access to explore', () => { + const { field, range } = setup( + { + title: 'external', + url: 'http://regionalhost', + }, + false + ); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range }); + + expect(links[0].href).toBe('http://regionalhost'); + expect(links[0].title).toBe('external'); + }); - it('returns internal links when target contains __data template variables', () => { - const { field, range, dataFrame } = setup({ + it('returns no internal links if when user does not have access to explore', () => { + const { field, range } = setup( + { title: '', url: '', internal: { - query: { query: 'query_1-${__data.fields.flux-dimensions}' }, + query: { query: 'query_1' }, datasourceUid: 'uid_1', datasourceName: 'test_ds', }, - }); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); - expect(links).toHaveLength(1); - expect(links[0].href).toBe( - `/explore?left=${encodeURIComponent( - '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' - )}` - ); + }, + false + ); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range }); + expect(links).toHaveLength(0); + }); + + it('returns internal links when target contains __data template variables', () => { + const { field, range, dataFrame } = setup({ + title: '', + url: '', + internal: { + query: { query: 'query_1-${__data.fields.flux-dimensions}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' + )}` + ); + }); - it('returns internal links when target contains targetField template variable', () => { - const { field, range, dataFrame } = setup({ - title: '', - url: '', - internal: { - query: { query: 'query_1-${__targetField}' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - }, - }); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); - expect(links).toHaveLength(1); - expect(links[0].href).toBe( - `/explore?left=${encodeURIComponent( - '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' - )}` - ); + it('returns internal links when target contains targetField template variable', () => { + const { field, range, dataFrame } = setup({ + title: '', + url: '', + internal: { + query: { query: 'query_1-${__targetField}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' + )}` + ); + }); - it('returns internal links when target contains field name template variable', () => { - // field cannot be hyphenated, change field name to non-hyphenated - const noHyphenLink = { - title: '', - url: '', - internal: { - query: { query: 'query_1-${fluxDimensions}' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - }, - }; - const { field, range, dataFrame } = setup(noHyphenLink, true, { + it('returns internal links when target contains field name template variable', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup(noHyphenLink, true, { + name: 'fluxDimensions', + type: FieldType.string, + values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' + )}` + ); + }); + + it('returns internal links when target contains other field name template variables', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}-${fluxDimension2}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup( + noHyphenLink, + true, + { name: 'fluxDimensions', type: FieldType.string, values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), config: { links: [noHyphenLink], }, - }); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); - expect(links).toHaveLength(1); - expect(links[0].href).toBe( - `/explore?left=${encodeURIComponent( - '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}' - )}` - ); - }); - - it('returns internal links when target contains other field name template variables', () => { - // field cannot be hyphenated, change field name to non-hyphenated - const noHyphenLink = { - title: '', - url: '', - internal: { - query: { query: 'query_1-${fluxDimensions}-${fluxDimension2}' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - }, - }; - const { field, range, dataFrame } = setup( - noHyphenLink, - true, + }, + [ { - name: 'fluxDimensions', + name: 'fluxDimension2', type: FieldType.string, - values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + values: new ArrayVector(['foo2', ROW_WITH_NULL_VALUE.value]), config: { links: [noHyphenLink], }, }, - [ - { - name: 'fluxDimension2', - type: FieldType.string, - values: new ArrayVector(['foo2', ROW_WITH_NULL_VALUE.value]), - config: { - links: [noHyphenLink], - }, - }, - ] - ); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); - expect(links).toHaveLength(1); - expect(links[0].href).toBe( - `/explore?left=${encodeURIComponent( - '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}]}' - )}` - ); + ] + ); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}]}' + )}` + ); + }); + + 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], + }, }); - 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 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}"}]}' + )}` + ); + }); - 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], + }, }); - 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 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}"}]}' + )}` + ); + }); - 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], + }, }); - 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 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}"}]}' + )}` + ); + }); - const { field, range, dataFrame } = setup(transformationLink, true, { - name: 'msg', + 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=foo online=true', 'application=bar online=false']), + values: new ArrayVector(['application=link', 'application=link2']), 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', + name: 'fieldNamedInTransformation', type: FieldType.string, - values: new ArrayVector(['application=link', 'application=link2']), - config: { - links: [transformationLink], - }, + 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: [ { - name: 'fieldNamedInTransformation', - type: FieldType.string, - values: new ArrayVector(['application=transform', 'application=transform2']), - config: {}, + type: SupportedTransformationTypes.Regex, + expression: '(?=.*(?(grafana|loki)))(?=.*(?(dev|prod)))', }, - ] - ); - - 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: '', - url: '', - internal: { - query: { query: 'query_1-${__data.fields.flux-dimensions}' }, - datasourceUid: 'uid_1', - datasourceName: 'test_ds', - }, - }); - const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame }); - expect(links).toHaveLength(0); + ], + }, + }; + + 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], + }, }); - it('does not return internal links when not all query variables are matched', () => { - const transformationLink: DataLink = { - title: '', - url: '', - internal: { - query: { query: 'http_requests{app=${application} env=${diffVar}}' }, - 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 host=dev-001']), - config: { - links: [transformationLink], - }, - }); - - const links = [getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame })]; - expect(links[0]).toHaveLength(0); - }); + 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}"}]}' + )}` + ); }); - describe('getVariableUsageInfo', () => { - it('returns true when query contains variables and all variables are used', () => { - const dataLink = { - url: '', - title: '', - internal: { - datasourceUid: 'uid', - datasourceName: 'dsName', - query: { query: 'test ${testVal}' }, - }, - }; - const scopedVars = { - testVal: { text: '', value: 'val1' }, - }; - const varMapMock = jest.fn().mockReturnValue({ testVal: scopedVars.testVal.value }); - const dataLinkRtnVal = getVariableUsageInfo(dataLink, scopedVars, varMapMock).allVariablesDefined; - - expect(dataLinkRtnVal).toBe(true); - }); - - it('returns false when query contains variables and no variables are used', () => { - const dataLink = { - url: '', - title: '', - internal: { - datasourceUid: 'uid', - datasourceName: 'dsName', - query: { query: 'test ${diffVar}' }, - }, - }; - const scopedVars = { - testVal: { text: '', value: 'val1' }, - }; - const varMapMock = jest.fn().mockReturnValue({ diffVar: null }); - const dataLinkRtnVal = getVariableUsageInfo(dataLink, scopedVars, varMapMock).allVariablesDefined; - - expect(dataLinkRtnVal).toBe(false); - }); - - it('returns false when query contains variables and some variables are used', () => { - const dataLink = { - url: '', - title: '', - internal: { - datasourceUid: 'uid', - datasourceName: 'dsName', - query: { query: 'test ${testVal} ${diffVar}' }, - }, - }; - const scopedVars = { - testVal: { text: '', value: 'val1' }, - }; - const varMapMock = jest.fn().mockReturnValue({ testVal: 'val1', diffVar: null }); - const dataLinkRtnVal = getVariableUsageInfo(dataLink, scopedVars, varMapMock).allVariablesDefined; - expect(dataLinkRtnVal).toBe(false); - }); - - it('returns true when query contains no variables', () => { - const dataLink = { - url: '', - title: '', - internal: { - datasourceUid: 'uid', - datasourceName: 'dsName', - query: { query: 'test' }, - }, - }; - const scopedVars = { - testVal: { text: '', value: 'val1' }, - }; - const varMapMock = jest.fn().mockReturnValue({}); - const dataLinkRtnVal = getVariableUsageInfo(dataLink, scopedVars, varMapMock).allVariablesDefined; - expect(dataLinkRtnVal).toBe(true); + it('returns no internal links when target contains empty template variables', () => { + const { field, range, dataFrame } = setup({ + title: '', + url: '', + internal: { + query: { query: 'query_1-${__data.fields.flux-dimensions}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(0); }); }); diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index fa129d13078..7e88f20222e 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -12,7 +12,6 @@ import { SplitOpen, DataLink, DisplayValue, - VariableMap, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -22,26 +21,40 @@ import { getLinkSrv } from '../../panel/panellinks/link_srv'; type DataLinkFilter = (link: DataLink, scopedVars: ScopedVars) => boolean; -const dataLinkHasRequiredPermissionsFilter = (link: DataLink) => { +const dataLinkHasRequiredPermissions = (link: DataLink) => { return !link.internal || contextSrv.hasAccessToExplore(); }; /** - * Fixed list of filters used in Explore. DataLinks that do not pass all the filters will not - * be passed back to the visualization. + * Check if every variable in the link has a value. If not this returns false. If there are no variables in the link + * this will return true. + * @param link + * @param scopedVars */ -const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasRequiredPermissionsFilter]; +const dataLinkHasAllVariablesDefined = (link: DataLink, scopedVars: ScopedVars) => { + let hasAllRequiredVarDefined = true; + + if (link.internal) { + let stringifiedQuery = ''; + try { + stringifiedQuery = JSON.stringify(link.internal.query || {}); + // Hook into format function to verify if all values are non-empty + // Format function is run on all existing field values allowing us to check it's value is non-empty + getTemplateSrv().replace(stringifiedQuery, scopedVars, (f: string) => { + hasAllRequiredVarDefined = hasAllRequiredVarDefined && !!f; + return ''; + }); + } catch (err) {} + } + + return hasAllRequiredVarDefined; +}; /** - * This extension of the LinkModel was done to support correlations, which need the variables' names - * and values split out for display purposes - * - * Correlations are internal links only so the variables property will always be defined (but possibly empty) - * for internal links and undefined for non-internal links + * Fixed list of filters used in Explore. DataLinks that do not pass all the filters will not + * be passed back to the visualization. */ -export interface ExploreFieldLinkModel extends LinkModel { - variables?: VariableMap; -} +const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasAllVariablesDefined, dataLinkHasRequiredPermissions]; /** * Get links from the field of a dataframe and in addition check if there is associated @@ -57,7 +70,7 @@ export const getFieldLinksForExplore = (options: { range: TimeRange; vars?: ScopedVars; dataFrame?: DataFrame; -}): ExploreFieldLinkModel[] => { +}): Array> => { const { field, vars, splitOpenFn, range, rowIndex, dataFrame } = options; const scopedVars: ScopedVars = { ...(vars || {}) }; scopedVars['__value'] = { @@ -104,7 +117,7 @@ export const getFieldLinksForExplore = (options: { return DATA_LINK_FILTERS.every((filter) => filter(link, scopedVars)); }); - const fieldLinks = links.map((link) => { + return links.map((link) => { if (!link.internal) { const replace: InterpolateFunction = (value, vars) => getTemplateSrv().replace(value, { ...vars, ...scopedVars }); @@ -133,35 +146,19 @@ export const getFieldLinksForExplore = (options: { }); } - const allVars = { ...scopedVars, ...internalLinkSpecificVars }; - const varMapFn = getTemplateSrv().getAllVariablesInTarget.bind(getTemplateSrv()); - const variableData = getVariableUsageInfo(link, allVars, varMapFn); - let variables: VariableMap = {}; - if (Object.keys(variableData.variables).length === 0) { - const fieldName = field.name.toString(); - variables[fieldName] = ''; - } else { - variables = variableData.variables; - } - - if (variableData.allVariablesDefined) { - const internalLink = mapInternalLinkToExplore({ - link, - internalLink: link.internal, - scopedVars: allVars, - range, - field, - onClickFn: splitOpenFn, - replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), - }); - return { ...internalLink, variables: variables }; - } else { - return undefined; - } + return mapInternalLinkToExplore({ + link, + internalLink: link.internal, + scopedVars: { ...scopedVars, ...internalLinkSpecificVars }, + range, + field, + onClickFn: splitOpenFn, + replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), + }); } }); - return fieldLinks.filter((link): link is ExploreFieldLinkModel => !!link); } + return []; }; @@ -207,36 +204,3 @@ export function useLinks(range: TimeRange, splitOpenFn?: SplitOpen) { [range, splitOpenFn] ); } - -/** - * Use variable map from templateSrv to determine if all variables have values - * @param query - * @param scopedVars - * @param getVarMap - */ -export function getVariableUsageInfo( - query: T, - scopedVars: ScopedVars, - getVarMap: Function -): { variables: VariableMap; allVariablesDefined: boolean } { - const vars = getVarMap(getStringsFromObject(query), scopedVars); - // the string processor will convert null to '' but is not ran in all scenarios - return { - variables: vars, - allVariablesDefined: Object.values(vars).every((val) => val !== undefined && val !== null && val !== ''), - }; -} - -function getStringsFromObject(obj: Object): string { - let acc = ''; - let k: keyof typeof obj; - - for (k in obj) { - if (typeof obj[k] === 'string') { - acc += ' ' + obj[k]; - } else if (typeof obj[k] === 'object') { - acc += ' ' + getStringsFromObject(obj[k]); - } - } - return acc; -} diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 1289b23991f..73f4a075500 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -8,7 +8,7 @@ import { calculateLogsLabelStats, calculateStats } from '../utils'; import { LogDetailsRow } from './LogDetailsRow'; import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles'; -import { getAllFields, createLogLineLinks } from './logParser'; +import { getAllFields } from './logParser'; export interface Props extends Themeable2 { row: LogRowModel; @@ -51,17 +51,10 @@ class UnThemedLogDetails extends PureComponent { const labels = row.labels ? row.labels : {}; const labelsAvailable = Object.keys(labels).length > 0; const fieldsAndLinks = getAllFields(row, getFieldLinks); - let fieldsWithLinks = fieldsAndLinks.filter((f) => f.links?.length); - const displayedFieldsWithLinks = fieldsWithLinks.filter((f) => f.fieldIndex !== row.entryFieldIndex).sort(); - const hiddenFieldsWithLinks = fieldsWithLinks.filter((f) => f.fieldIndex === row.entryFieldIndex).sort(); - const fieldsWithLinksFromVariableMap = createLogLineLinks(hiddenFieldsWithLinks); - - // do not show the log message unless there is a link attached - const fields = fieldsAndLinks.filter((f) => f.links?.length === 0 && f.fieldIndex !== row.entryFieldIndex).sort(); + const links = fieldsAndLinks.filter((f) => f.links?.length).sort(); + const fields = fieldsAndLinks.filter((f) => f.links?.length === 0).sort(); const fieldsAvailable = fields && fields.length > 0; - const fieldsWithLinksAvailable = - (displayedFieldsWithLinks && displayedFieldsWithLinks.length > 0) || - (fieldsWithLinksFromVariableMap && fieldsWithLinksFromVariableMap.length > 0); + const linksAvailable = links && links.length > 0; // If logs with error, we are not showing the level color const levelClassName = hasError @@ -85,13 +78,13 @@ class UnThemedLogDetails extends PureComponent { )} {Object.keys(labels) .sort() - .map((key, i) => { + .map((key) => { const value = labels[key]; return ( calculateLogsLabelStats(getRows(), key)} onClickFilterOutLabel={onClickFilterOutLabel} @@ -102,17 +95,16 @@ class UnThemedLogDetails extends PureComponent { app={app} wrapLogMessage={wrapLogMessage} displayedFields={displayedFields} - disableActions={false} /> ); })} - {fields.map((field, i) => { - const { keys, values, fieldIndex } = field; + {fields.map((field) => { + const { key, value, fieldIndex } = field; return ( { wrapLogMessage={wrapLogMessage} row={row} app={app} - disableActions={false} /> ); })} - {fieldsWithLinksAvailable && ( + {linksAvailable && ( Links )} - {displayedFieldsWithLinks.map((field, i) => { - const { keys, values, links, fieldIndex } = field; + {links.map((field) => { + const { key, value, links, fieldIndex } = field; return ( { wrapLogMessage={wrapLogMessage} row={row} app={app} - disableActions={false} /> ); })} - {fieldsWithLinksFromVariableMap?.map((field, i) => { - const { keys, values, links, fieldIndex } = field; - return ( - calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())} - displayedFields={displayedFields} - wrapLogMessage={wrapLogMessage} - row={row} - app={app} - disableActions={true} - /> - ); - })} - - {!fieldsAvailable && !labelsAvailable && !fieldsWithLinksAvailable && ( + {!fieldsAvailable && !labelsAvailable && !linksAvailable && ( No details available diff --git a/public/app/features/logs/components/LogDetailsRow.test.tsx b/public/app/features/logs/components/LogDetailsRow.test.tsx index 17fbbc810fd..e4bda4cf3e8 100644 --- a/public/app/features/logs/components/LogDetailsRow.test.tsx +++ b/public/app/features/logs/components/LogDetailsRow.test.tsx @@ -9,8 +9,8 @@ type Props = ComponentProps; const setup = (propOverrides?: Partial) => { const props: Props = { - parsedValues: [''], - parsedKeys: [''], + parsedValue: '', + parsedKey: '', isLabel: true, wrapLogMessage: false, getStats: () => null, @@ -20,7 +20,6 @@ const setup = (propOverrides?: Partial) => { onClickHideField: () => {}, displayedFields: [], row: {} as LogRowModel, - disableActions: false, }; Object.assign(props, propOverrides); @@ -41,11 +40,11 @@ jest.mock('@grafana/runtime', () => ({ describe('LogDetailsRow', () => { it('should render parsed key', () => { - setup({ parsedKeys: ['test key'] }); + setup({ parsedKey: 'test key' }); expect(screen.getByText('test key')).toBeInTheDocument(); }); it('should render parsed value', () => { - setup({ parsedValues: ['test value'] }); + setup({ parsedValue: 'test value' }); expect(screen.getByText('test value')).toBeInTheDocument(); }); @@ -74,8 +73,8 @@ describe('LogDetailsRow', () => { it('should render stats when stats icon is clicked', () => { setup({ - parsedKeys: ['key'], - parsedValues: ['value'], + parsedKey: 'key', + parsedValue: 'value', getStats: () => { return [ { diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 38ee2727e63..f1230d64001 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -13,9 +13,8 @@ import { getLogRowStyles } from './getLogRowStyles'; //Components export interface Props extends Themeable2 { - parsedValues: string[]; - parsedKeys: string[]; - disableActions: boolean; + parsedValue: string; + parsedKey: string; wrapLogMessage?: boolean; isLabel?: boolean; onClickFilterLabel?: (key: string, value: string) => void; @@ -61,9 +60,6 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => { } } `, - adjoiningLinkButton: css` - margin-left: ${theme.spacing(1)}; - `, wrapLine: css` label: wrapLine; white-space: pre-wrap; @@ -72,8 +68,8 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => { padding: 0 ${theme.spacing(1)}; `, logDetailsValue: css` - display: flex; - align-items: center; + display: table-cell; + vertical-align: middle; line-height: 22px; .show-on-hover { @@ -109,9 +105,9 @@ class UnThemedLogDetailsRow extends PureComponent { } showField = () => { - const { onClickShowField: onClickShowDetectedField, parsedKeys, row } = this.props; + const { onClickShowField: onClickShowDetectedField, parsedKey, row } = this.props; if (onClickShowDetectedField) { - onClickShowDetectedField(parsedKeys[0]); + onClickShowDetectedField(parsedKey); } reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { @@ -122,9 +118,9 @@ class UnThemedLogDetailsRow extends PureComponent { }; hideField = () => { - const { onClickHideField: onClickHideDetectedField, parsedKeys, row } = this.props; + const { onClickHideField: onClickHideDetectedField, parsedKey, row } = this.props; if (onClickHideDetectedField) { - onClickHideDetectedField(parsedKeys[0]); + onClickHideDetectedField(parsedKey); } reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { @@ -135,9 +131,9 @@ class UnThemedLogDetailsRow extends PureComponent { }; filterLabel = () => { - const { onClickFilterLabel, parsedKeys, parsedValues, row } = this.props; + const { onClickFilterLabel, parsedKey, parsedValue, row } = this.props; if (onClickFilterLabel) { - onClickFilterLabel(parsedKeys[0], parsedValues[0]); + onClickFilterLabel(parsedKey, parsedValue); } reportInteraction('grafana_explore_logs_log_details_filter_clicked', { @@ -148,9 +144,9 @@ class UnThemedLogDetailsRow extends PureComponent { }; filterOutLabel = () => { - const { onClickFilterOutLabel, parsedKeys, parsedValues, row } = this.props; + const { onClickFilterOutLabel, parsedKey, parsedValue, row } = this.props; if (onClickFilterOutLabel) { - onClickFilterOutLabel(parsedKeys[0], parsedValues[0]); + onClickFilterOutLabel(parsedKey, parsedValue); } reportInteraction('grafana_explore_logs_log_details_filter_clicked', { @@ -194,68 +190,25 @@ class UnThemedLogDetailsRow extends PureComponent { }); } - generateClipboardButton(val: string) { - const { theme } = this.props; - const styles = getStyles(theme); - - return ( -
- val} - title="Copy value to clipboard" - fill="text" - variant="secondary" - icon="copy" - size="md" - /> -
- ); - } - - generateMultiVal(value: string[], showCopy?: boolean) { - return ( - - - {value?.map((val, i) => { - return ( - - - - ); - })} - -
- {val} - {showCopy && val !== '' && this.generateClipboardButton(val)} -
- ); - } - render() { const { theme, - parsedKeys, - parsedValues, + parsedKey, + parsedValue, isLabel, links, displayedFields, wrapLogMessage, onClickFilterLabel, onClickFilterOutLabel, - disableActions, } = this.props; const { showFieldsStats, fieldStats, fieldCount } = this.state; const styles = getStyles(theme); const style = getLogRowStyles(theme); - const singleKey = parsedKeys == null ? false : parsedKeys.length === 1; - const singleVal = parsedValues == null ? false : parsedValues.length === 1; - const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel; - - const isMultiParsedValueWithNoContent = - !singleVal && parsedValues != null && !parsedValues.every((val) => val === ''); + const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel; const toggleFieldButton = - displayedFields && parsedKeys != null && displayedFields.includes(parsedKeys[0]) ? ( + displayedFields && displayedFields.includes(parsedKey) ? ( ) : ( @@ -272,37 +225,44 @@ class UnThemedLogDetailsRow extends PureComponent { {hasFilteringFunctionality && ( )} - {!disableActions && displayedFields && toggleFieldButton} - {!disableActions && ( - - )} + {displayedFields && toggleFieldButton} + {/* Key - value columns */} - {singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)} + {parsedKey}
- {singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)} - {singleVal && this.generateClipboardButton(parsedValues[0])} -
- {links?.map((link, i) => ( - - - - ))} + {parsedValue} + +
+ parsedValue} + title="Copy value to clipboard" + fill="text" + variant="secondary" + icon="copy" + size="md" + />
+ + {links?.map((link) => ( + +   + + + ))}
- {showFieldsStats && singleKey && singleVal && ( + {showFieldsStats && ( {
diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index cf42745a187..5394beca55e 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -22,16 +22,16 @@ class UnThemedLogRowMessageDisplayedFields extends PureComponent { : css` white-space: nowrap; `; - // only single key/value rows are filterable, so we only need the first field key for filtering + const line = showDetectedFields .map((parsedKey) => { const field = fields.find((field) => { - const { keys } = field; - return keys[0] === parsedKey; + const { key } = field; + return key === parsedKey; }); if (field !== undefined && field !== null) { - return `${parsedKey}=${field.values}`; + return `${parsedKey}=${field.value}`; } if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { diff --git a/public/app/features/logs/components/logParser.test.ts b/public/app/features/logs/components/logParser.test.ts index cf6ee67ad7c..7c261805832 100644 --- a/public/app/features/logs/components/logParser.test.ts +++ b/public/app/features/logs/components/logParser.test.ts @@ -1,212 +1,155 @@ import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data'; -import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; import { createLogRow } from './__mocks__/logRow'; -import { getAllFields, createLogLineLinks, FieldDef } from './logParser'; - -describe('logParser', () => { - describe('getAllFields', () => { - it('should filter out field with labels name and other type', () => { - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [ - testStringField, - { - name: 'labels', - type: FieldType.other, - config: {}, - values: new ArrayVector([{ place: 'luna', source: 'data' }]), - }, - ], - }), - }); - - const fields = getAllFields(logRow); - expect(fields.length).toBe(1); - expect(fields.find((field) => field.keys[0] === 'labels')).toBe(undefined); +import { getAllFields } from './logParser'; + +describe('getAllFields', () => { + it('should filter out field with labels name and other type', () => { + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [ + testStringField, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: new ArrayVector([{ place: 'luna', source: 'data' }]), + }, + ], + }), }); - it('should not filter out field with labels name and string type', () => { - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [ - testStringField, - { - name: 'labels', - type: FieldType.string, - config: {}, - values: new ArrayVector([{ place: 'luna', source: 'data' }]), - }, - ], - }), - }); - const fields = getAllFields(logRow); - expect(fields.length).toBe(2); - expect(fields.find((field) => field.keys[0] === 'labels')).not.toBe(undefined); - }); + const fields = getAllFields(logRow); + expect(fields.length).toBe(1); + expect(fields.find((field) => field.key === 'labels')).toBe(undefined); + }); - it('should filter out field with id name', () => { - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [ - testStringField, - { - name: 'id', - type: FieldType.string, - config: {}, - values: new ArrayVector(['1659620138401000000_8b1f7688_']), - }, - ], - }), - }); - - const fields = getAllFields(logRow); - expect(fields.length).toBe(1); - expect(fields.find((field) => field.keys[0] === 'id')).toBe(undefined); + it('should not filter out field with labels name and string type', () => { + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [ + testStringField, + { + name: 'labels', + type: FieldType.string, + config: {}, + values: new ArrayVector([{ place: 'luna', source: 'data' }]), + }, + ], + }), }); + const fields = getAllFields(logRow); + expect(fields.length).toBe(2); + expect(fields.find((field) => field.key === 'labels')).not.toBe(undefined); + }); - it('should filter out field with config hidden field', () => { - const testField = { ...testStringField }; - testField.config = { - custom: { - hidden: true, - }, - }; - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [{ ...testField }], - }), - }); - - const fields = getAllFields(logRow); - expect(fields.length).toBe(0); - expect(fields.find((field) => field.keys[0] === testField.name)).toBe(undefined); + it('should filter out field with id name', () => { + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [ + testStringField, + { + name: 'id', + type: FieldType.string, + config: {}, + values: new ArrayVector(['1659620138401000000_8b1f7688_']), + }, + ], + }), }); - it('should filter out field with null values', () => { - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [{ ...testFieldWithNullValue }], - }), - }); - - const fields = getAllFields(logRow); - expect(fields.length).toBe(0); - expect(fields.find((field) => field.keys[0] === testFieldWithNullValue.name)).toBe(undefined); - }); + const fields = getAllFields(logRow); + expect(fields.length).toBe(1); + expect(fields.find((field) => field.key === 'id')).toBe(undefined); + }); - it('should not filter out field with string values', () => { - const logRow = createLogRow({ - entryFieldIndex: 10, - dataFrame: new MutableDataFrame({ - refId: 'A', - fields: [{ ...testStringField }], - }), - }); - - const fields = getAllFields(logRow); - expect(fields.length).toBe(1); - expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined); + it('should filter out entry field which is shown as the log message', () => { + const logRow = createLogRow({ + entryFieldIndex: 3, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [ + testStringField, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: new ArrayVector([{ place: 'luna', source: 'data' }]), + }, + { + name: 'Time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1659620138401]), + }, + { + name: 'Line', + type: FieldType.string, + config: {}, + values: new ArrayVector([ + '_entry="log text with ANSI \u001b[31mpart of the text\u001b[0m [616951240]" counter=300 float=NaN label=val3 level=info', + ]), + }, + ], + }), }); + + const fields = getAllFields(logRow); + expect(fields.find((field) => field.key === 'Line')).toBe(undefined); }); - describe('createLogLineLinks', () => { - it('should change FieldDef to have keys of variable keys', () => { - const variableLink: ExploreFieldLinkModel = { - href: 'test', - onClick: () => {}, - origin: { - config: { links: [] }, - name: 'Line', - type: FieldType.string, - values: new ArrayVector(['a', 'b']), - }, - title: 'test', - target: '_self', - variables: { path: 'test', msg: 'test msg' }, - }; - - const fieldWithVarLink: FieldDef = { - fieldIndex: 2, - keys: ['Line'], - values: ['level=info msg="test msg" status_code=200 url=http://test'], - links: [variableLink], - }; - - const fields = createLogLineLinks([fieldWithVarLink]); - expect(fields.length).toBe(1); - expect(fields[0].keys.length).toBe(2); - expect(fields[0].keys[0]).toBe('path'); - expect(fields[0].values[0]).toBe('test'); - expect(fields[0].keys[1]).toBe('msg'); - expect(fields[0].values[1]).toBe('test msg'); + it('should filter out field with config hidden field', () => { + const testField = { ...testStringField }; + testField.config = { + custom: { + hidden: true, + }, + }; + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [{ ...testField }], + }), }); - it('should convert null value to empty string and non string to string', () => { - const variableLink: ExploreFieldLinkModel = { - href: 'test', - onClick: () => {}, - origin: { - config: { links: [] }, - name: 'Line', - type: FieldType.string, - values: new ArrayVector(['a', 'b']), - }, - title: 'test', - target: '_self', - variables: { path: undefined, message: false }, - }; - - const fieldWithVarLink: FieldDef = { - fieldIndex: 2, - keys: ['Line'], - values: ['level=info msg="test msg" status_code=200 url=http://test'], - links: [variableLink], - }; - - const fields = createLogLineLinks([fieldWithVarLink]); - expect(fields.length).toBe(1); - expect(fields[0].keys.length).toBe(2); - expect(fields[0].keys[0]).toBe('path'); - expect(fields[0].values[0]).toBe(''); - expect(fields[0].keys[1]).toBe('message'); - expect(fields[0].values[1]).toBe('false'); + const fields = getAllFields(logRow); + expect(fields.length).toBe(0); + expect(fields.find((field) => field.key === testField.name)).toBe(undefined); + }); + + it('should filter out field with null values', () => { + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [{ ...testFieldWithNullValue }], + }), }); - it('should return empty array if no variables', () => { - const variableLink: ExploreFieldLinkModel = { - href: 'test', - onClick: () => {}, - origin: { - config: { links: [] }, - name: 'Line', - type: FieldType.string, - values: new ArrayVector(['a', 'b']), - }, - title: 'test', - target: '_self', - }; - - const fieldWithVarLink: FieldDef = { - fieldIndex: 2, - keys: ['Line'], - values: ['level=info msg="test msg" status_code=200 url=http://test'], - links: [variableLink], - }; - - const fields = createLogLineLinks([fieldWithVarLink]); - expect(fields.length).toBe(0); + const fields = getAllFields(logRow); + expect(fields.length).toBe(0); + expect(fields.find((field) => field.key === testFieldWithNullValue.name)).toBe(undefined); + }); + + it('should not filter out field with string values', () => { + const logRow = createLogRow({ + entryFieldIndex: 10, + dataFrame: new MutableDataFrame({ + refId: 'A', + fields: [{ ...testStringField }], + }), }); + + const fields = getAllFields(logRow); + expect(fields.length).toBe(1); + expect(fields.find((field) => field.key === testStringField.name)).not.toBe(undefined); }); }); diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index b8296d24dd9..22359e26080 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,12 +1,11 @@ import memoizeOne from 'memoize-one'; import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data'; -import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; -export type FieldDef = { - keys: string[]; - values: string[]; - links?: Array> | ExploreFieldLinkModel[]; +type FieldDef = { + key: string; + value: string; + links?: Array>; fieldIndex: number; }; @@ -17,11 +16,7 @@ export type FieldDef = { export const getAllFields = memoizeOne( ( row: LogRowModel, - getFieldLinks?: ( - field: Field, - rowIndex: number, - dataFrame: DataFrame - ) => Array> | ExploreFieldLinkModel[] + getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> ) => { const dataframeFields = getDataframeFields(row, getFieldLinks); @@ -29,31 +24,6 @@ export const getAllFields = memoizeOne( } ); -/** - * A log line may contain many links that would all need to go on their own logs detail row - * This iterates through and creates a FieldDef (row) per link. - */ -export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { - let fieldsWithLinksFromVariableMap: FieldDef[] = []; - hiddenFieldsWithLinks.forEach((linkField) => { - linkField.links?.forEach((link: ExploreFieldLinkModel) => { - if (link.variables) { - const variableKeys = Object.keys(link.variables); - const variableValues = Object.keys(link.variables).map((key) => - link.variables && link.variables[key] != null ? link.variables[key]!.toString() : '' - ); - fieldsWithLinksFromVariableMap.push({ - keys: variableKeys, - values: variableValues, - links: [link], - fieldIndex: linkField.fieldIndex, - }); - } - }); - }); - return fieldsWithLinksFromVariableMap; -}); - /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ @@ -68,8 +38,8 @@ export const getDataframeFields = memoizeOne( .map((field) => { const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; return { - keys: [field.name], - values: [field.values.get(row.rowIndex).toString()], + key: field.name, + value: field.values.get(row.rowIndex).toString(), links: links, fieldIndex: field.index, }; @@ -87,6 +57,10 @@ function shouldRemoveField(field: Field, index: number, row: LogRowModel) { if (field.name === 'id' || field.name === 'tsNs') { return true; } + // entry field which we are showing as the log message + if (row.entryFieldIndex === index) { + return true; + } const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time); if ( field.name === firstTimeField?.name && diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 68cdd4c070e..66319a9931e 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -135,8 +135,7 @@ export const escapeUnescapedString = (string: string) => export function logRowsToReadableJson(logs: LogRowModel[]) { return logs.map((log) => { const fields = getDataframeFields(log).reduce>((acc, field) => { - const key = field.keys[0]; - acc[key] = field.values[0]; + acc[field.key] = field.value; return acc; }, {}); diff --git a/public/app/features/templating/template_srv.mock.ts b/public/app/features/templating/template_srv.mock.ts index b8c47cb2c08..70ab70fb988 100644 --- a/public/app/features/templating/template_srv.mock.ts +++ b/public/app/features/templating/template_srv.mock.ts @@ -40,21 +40,6 @@ export class TemplateSrvMock implements TemplateSrv { }); } - getAllVariablesInTarget(target: string, scopedVars: ScopedVars): Record { - const regexp = new RegExp(this.regex); - const values: Record = {}; - - target.replace(regexp, (match, var1, var2, fmt2, var3, fieldPath) => { - const variableName = var1 || var2 || var3; - values[variableName] = this.variables[variableName]; - - // Don't care about the result anyway - return ''; - }); - - return values; - } - getVariableName(expression: string) { this.regex.lastIndex = 0; const match = this.regex.exec(expression); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 4ecd56515be..2368b5ae9ac 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -7,7 +7,6 @@ import { AdHocVariableFilter, AdHocVariableModel, TypedVariableModel, - VariableMap, } from '@grafana/data'; import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; import { sceneGraph, FormatRegistryID, formatRegistry, VariableCustomFormatterFn } from '@grafana/scenes'; @@ -352,51 +351,6 @@ export class TemplateSrv implements BaseTemplateSrv { }); } - getAllVariablesInTarget(target: string, scopedVars: ScopedVars, format?: string | Function): VariableMap { - const values: VariableMap = {}; - - this.replaceInVariableRegex(target, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { - const variableName = var1 || var2 || var3; - const variableDisplayName = - var1 || var2 || (var3 !== undefined && fieldPath !== undefined) ? `${var3}.${fieldPath}` : var3; - const fmt = fmt2 || fmt3 || format; - const value = this.getVariableValue(variableName, fieldPath, scopedVars); - if (value !== null && value !== undefined) { - const variable = this.getVariableAtIndex(variableName); - const text = this.getVariableText(variableName, value, scopedVars); - values[variableDisplayName] = this.formatValue(value, fmt, variable, text); - } else { - values[variableDisplayName] = undefined; - } - - // Don't care about the result anyway - return ''; - }); - - return values; - } - - /** - * The replace function, for every match, will return a function that has the full match as a param - * followed by one param per capture group of the variable regex. - * - * See the definition of this.regex for further comments on the variable definitions. - */ - private replaceInVariableRegex( - text: string, - replace: ( - fullMatch: string, // $simpleVarName [[squareVarName:squareFormat]] ${curlyVarName.curlyPath:curlyFormat} - simpleVarName: string, // simpleVarName - - - squareVarName: string, // - squareVarName - - squareFormat: string, // - squareFormat - - curlyVarName: string, // - - curlyVarName - curlyPath: string, // - - curlyPath - curlyFormat: string // - - curlyFormat - ) => string - ) { - return text.replace(this.regex, replace); - } - isAllValue(value: any) { return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE); } diff --git a/public/app/features/variables/utils.test.ts b/public/app/features/variables/utils.test.ts index 5f3e21039fd..4c22a0fc842 100644 --- a/public/app/features/variables/utils.test.ts +++ b/public/app/features/variables/utils.test.ts @@ -185,19 +185,17 @@ describe('ensureStringValues', () => { describe('containsVariable', () => { it.each` - value | expected - ${''} | ${false} - ${'$var'} | ${true} - ${{ thing1: '${var}' }} | ${true} - ${{ thing1: '${var:fmt}' }} | ${true} - ${{ thing1: '${var.fieldPath}' }} | ${true} - ${{ thing1: '${var.fieldPath:fmt}' }} | ${true} - ${{ thing1: ['1', '${var}'] }} | ${true} - ${{ thing1: ['1', '[[var]]'] }} | ${true} - ${{ thing1: ['1', '[[var:fmt]]'] }} | ${true} - ${{ thing1: { thing2: '${var}' } }} | ${true} - ${{ params: [['param', '$var']] }} | ${true} - ${{ params: [['param', '${var}']] }} | ${true} + value | expected + ${''} | ${false} + ${'$var'} | ${true} + ${{ thing1: '${var}' }} | ${true} + ${{ thing1: '${var:fmt}' }} | ${true} + ${{ thing1: ['1', '${var}'] }} | ${true} + ${{ thing1: ['1', '[[var]]'] }} | ${true} + ${{ thing1: ['1', '[[var:fmt]]'] }} | ${true} + ${{ thing1: { thing2: '${var}' } }} | ${true} + ${{ params: [['param', '$var']] }} | ${true} + ${{ params: [['param', '${var}']] }} | ${true} `('when called with value:$value then result should be:$expected', ({ value, expected }) => { expect(containsVariable(value, 'var')).toEqual(expected); }); diff --git a/public/app/features/variables/utils.ts b/public/app/features/variables/utils.ts index a73bce617a1..c0f2d3a865b 100644 --- a/public/app/features/variables/utils.ts +++ b/public/app/features/variables/utils.ts @@ -16,10 +16,9 @@ import { QueryVariableModel, TransactionStatus, VariableModel, VariableRefresh, /* * This regex matches 3 types of variable reference with an optional format specifier - * There are 6 capture groups that replace will return - * \$(\w+) $var1 - * \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] - * \${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?} ${var3} or ${var3.fieldPath} or ${var3:fmt3} (or ${var3.fieldPath:fmt3} but that is not a separate capture group) + * \$(\w+) $var1 + * \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} */ export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; diff --git a/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx b/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx index b001350e3ad..6eb14771d10 100644 --- a/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx +++ b/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx @@ -38,9 +38,6 @@ describe('DebugSection', () => { getVariables() { return []; }, - getAllVariablesInTarget(target, scopedVars) { - return {}; - }, containsTemplate() { return false; }, diff --git a/public/app/plugins/datasource/zipkin/datasource.test.ts b/public/app/plugins/datasource/zipkin/datasource.test.ts index ba9734fcd12..99cb2ff5a1c 100644 --- a/public/app/plugins/datasource/zipkin/datasource.test.ts +++ b/public/app/plugins/datasource/zipkin/datasource.test.ts @@ -21,7 +21,6 @@ describe('ZipkinDatasource', () => { replace: jest.fn(), getVariables: jest.fn(), containsTemplate: jest.fn(), - getAllVariablesInTarget: jest.fn(), updateTimeRange: jest.fn(), };