mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Show log line if there is an interpolated link (#62926)
* 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
* Add provisioning (to be removed) and show log lines with links
* Only display links if change to query was made
* 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
* Change transformation types to enum, add better provisioning checks for bad type name and format
* Check for expression with regex transformations
* Remove isInterpolated variable, add option to always use format function
* Add template variable check to links
* Use new functions
* Filter log line at render, remove extra createSpanLink imports
* Add scrollable to long log messages
* Remove test that is no longer accurate
* Remove test correlation
* Add tests, fix duplicate key issue
* WIP: show log line links key/value pairs
* Some not great style changes
* Change LogDetailsRow for better multi value formatting
* Cleanup
* Add additional information around variable regex, implement PR feedback
* Display name with fieldPath if applicable
* Add variables with fieldPaths to test
* Count empty string as undefined variable
* Add better commented version of function, fix tests by removing new variable
* Modify when links show
* Remove sample yaml
* If a link has no variables, set value to field name, and some formatting issues
* Add comments and change variable names to be more clear, add back logic where needed, add test coverage for new scenario
* Fix formatting of replaceInVariableRegex comment
* Remove changes from Grafana-data, move logic into explore
* Rename function and property to match similar format
* Move types to type files and consolidate definitions, rename functions, change field definitions to accept arrays of keys/values, move function to parser, hide actions on multi key/value rows
* Add tests to logParser’s new function
This commit is contained in:
parent
a1fc515c88
commit
aa857e2a4f
@ -22,6 +22,10 @@ export type TypedVariableModel =
|
|||||||
| OrgVariableModel
|
| OrgVariableModel
|
||||||
| DashboardVariableModel;
|
| DashboardVariableModel;
|
||||||
|
|
||||||
|
type VarValue = string | number | boolean | undefined;
|
||||||
|
|
||||||
|
export type VariableMap = Record<string, VarValue>;
|
||||||
|
|
||||||
export enum VariableRefresh {
|
export enum VariableRefresh {
|
||||||
never, // removed from the UI
|
never, // removed from the UI
|
||||||
onDashboardLoad,
|
onDashboardLoad,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data';
|
import { ScopedVars, TimeRange, TypedVariableModel, VariableMap } from '@grafana/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Via the TemplateSrv consumers get access to all the available template variables
|
* Via the TemplateSrv consumers get access to all the available template variables
|
||||||
@ -18,6 +18,11 @@ export interface TemplateSrv {
|
|||||||
*/
|
*/
|
||||||
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string;
|
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.
|
* Checks if a target contains template variables.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { property } from 'lodash';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -24,8 +23,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|||||||
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
||||||
|
|
||||||
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
||||||
import { variableRegex } from '../../variables/utils';
|
import { getFieldLinksForExplore, getVariableUsageInfo } from '../utils/links';
|
||||||
import { getFieldLinksForExplore } from '../utils/links';
|
|
||||||
|
|
||||||
import { SpanLinkFunc, Trace, TraceSpan } from './components';
|
import { SpanLinkFunc, Trace, TraceSpan } from './components';
|
||||||
import { SpanLinks } from './components/types/links';
|
import { SpanLinks } from './components/types/links';
|
||||||
@ -192,7 +190,13 @@ function legacyCreateSpanLinkFactory(
|
|||||||
|
|
||||||
// Check if all variables are defined and don't show if they aren't. This is usually handled by the
|
// 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.
|
// getQueryFor* functions but this is for case of custom query supplied by the user.
|
||||||
if (dataLinkHasAllVariablesDefined(dataLink.internal!.query, scopedVars)) {
|
if (
|
||||||
|
getVariableUsageInfo(
|
||||||
|
dataLink.internal!.query,
|
||||||
|
scopedVars,
|
||||||
|
getTemplateSrv().getAllVariablesInTarget.bind(getTemplateSrv())
|
||||||
|
).allVariablesDefined
|
||||||
|
) {
|
||||||
const link = mapInternalLinkToExplore({
|
const link = mapInternalLinkToExplore({
|
||||||
link: dataLink,
|
link: dataLink,
|
||||||
internalLink: dataLink.internal!,
|
internalLink: dataLink.internal!,
|
||||||
@ -576,65 +580,3 @@ 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<T extends DataQuery>(query: T, scopedVars: ScopedVars): boolean {
|
|
||||||
const vars = getVariablesMapInTemplate(getStringsFromObject(query), scopedVars);
|
|
||||||
return Object.values(vars).every((val) => val !== undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStringsFromObject<T extends Object>(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<string, VarValue> {
|
|
||||||
const regex = new RegExp(variableRegex);
|
|
||||||
const values: Record<string, VarValue> = {};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
@ -16,9 +16,10 @@ import { initTemplateSrv } from '../../../../test/helpers/initTemplateSrv';
|
|||||||
import { ContextSrv, setContextSrv } from '../../../core/services/context_srv';
|
import { ContextSrv, setContextSrv } from '../../../core/services/context_srv';
|
||||||
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
|
|
||||||
import { getFieldLinksForExplore } from './links';
|
import { getFieldLinksForExplore, getVariableUsageInfo } from './links';
|
||||||
|
|
||||||
describe('getFieldLinksForExplore', () => {
|
describe('explore links utils', () => {
|
||||||
|
describe('getFieldLinksForExplore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setTemplateSrv(
|
setTemplateSrv(
|
||||||
initTemplateSrv('key', [
|
initTemplateSrv('key', [
|
||||||
@ -77,7 +78,12 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const splitfn = jest.fn();
|
const splitfn = jest.fn();
|
||||||
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, splitOpenFn: splitfn, range });
|
const links = getFieldLinksForExplore({
|
||||||
|
field,
|
||||||
|
rowIndex: ROW_WITH_TEXT_VALUE.index,
|
||||||
|
splitOpenFn: splitfn,
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
expect(links[0].href).toBe(
|
expect(links[0].href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
@ -481,6 +487,108 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame });
|
const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_NULL_VALUE.index, range, dataFrame });
|
||||||
expect(links).toHaveLength(0);
|
expect(links).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 };
|
const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 };
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
SplitOpen,
|
SplitOpen,
|
||||||
DataLink,
|
DataLink,
|
||||||
DisplayValue,
|
DisplayValue,
|
||||||
|
VariableMap,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
@ -21,40 +22,26 @@ import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
|||||||
|
|
||||||
type DataLinkFilter = (link: DataLink, scopedVars: ScopedVars) => boolean;
|
type DataLinkFilter = (link: DataLink, scopedVars: ScopedVars) => boolean;
|
||||||
|
|
||||||
const dataLinkHasRequiredPermissions = (link: DataLink) => {
|
const dataLinkHasRequiredPermissionsFilter = (link: DataLink) => {
|
||||||
return !link.internal || contextSrv.hasAccessToExplore();
|
return !link.internal || contextSrv.hasAccessToExplore();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixed list of filters used in Explore. DataLinks that do not pass all the filters will not
|
* Fixed list of filters used in Explore. DataLinks that do not pass all the filters will not
|
||||||
* be passed back to the visualization.
|
* be passed back to the visualization.
|
||||||
*/
|
*/
|
||||||
const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasAllVariablesDefined, dataLinkHasRequiredPermissions];
|
const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasRequiredPermissionsFilter];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export interface ExploreFieldLinkModel extends LinkModel<Field> {
|
||||||
|
variables?: VariableMap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get links from the field of a dataframe and in addition check if there is associated
|
* Get links from the field of a dataframe and in addition check if there is associated
|
||||||
@ -70,7 +57,7 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
vars?: ScopedVars;
|
vars?: ScopedVars;
|
||||||
dataFrame?: DataFrame;
|
dataFrame?: DataFrame;
|
||||||
}): Array<LinkModel<Field>> => {
|
}): ExploreFieldLinkModel[] => {
|
||||||
const { field, vars, splitOpenFn, range, rowIndex, dataFrame } = options;
|
const { field, vars, splitOpenFn, range, rowIndex, dataFrame } = options;
|
||||||
const scopedVars: ScopedVars = { ...(vars || {}) };
|
const scopedVars: ScopedVars = { ...(vars || {}) };
|
||||||
scopedVars['__value'] = {
|
scopedVars['__value'] = {
|
||||||
@ -117,7 +104,7 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
return DATA_LINK_FILTERS.every((filter) => filter(link, scopedVars));
|
return DATA_LINK_FILTERS.every((filter) => filter(link, scopedVars));
|
||||||
});
|
});
|
||||||
|
|
||||||
return links.map((link) => {
|
const fieldLinks = links.map((link) => {
|
||||||
if (!link.internal) {
|
if (!link.internal) {
|
||||||
const replace: InterpolateFunction = (value, vars) =>
|
const replace: InterpolateFunction = (value, vars) =>
|
||||||
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
||||||
@ -146,19 +133,35 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapInternalLinkToExplore({
|
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,
|
link,
|
||||||
internalLink: link.internal,
|
internalLink: link.internal,
|
||||||
scopedVars: { ...scopedVars, ...internalLinkSpecificVars },
|
scopedVars: allVars,
|
||||||
range,
|
range,
|
||||||
field,
|
field,
|
||||||
onClickFn: splitOpenFn,
|
onClickFn: splitOpenFn,
|
||||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||||
});
|
});
|
||||||
|
return { ...internalLink, variables: variables };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return fieldLinks.filter((link): link is ExploreFieldLinkModel => !!link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,3 +207,36 @@ export function useLinks(range: TimeRange, splitOpenFn?: SplitOpen) {
|
|||||||
[range, splitOpenFn]
|
[range, splitOpenFn]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use variable map from templateSrv to determine if all variables have values
|
||||||
|
* @param query
|
||||||
|
* @param scopedVars
|
||||||
|
* @param getVarMap
|
||||||
|
*/
|
||||||
|
export function getVariableUsageInfo<T extends DataLink>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { calculateLogsLabelStats, calculateStats } from '../utils';
|
|||||||
|
|
||||||
import { LogDetailsRow } from './LogDetailsRow';
|
import { LogDetailsRow } from './LogDetailsRow';
|
||||||
import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles';
|
import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles';
|
||||||
import { getAllFields } from './logParser';
|
import { getAllFields, createLogLineLinks } from './logParser';
|
||||||
|
|
||||||
export interface Props extends Themeable2 {
|
export interface Props extends Themeable2 {
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
@ -51,10 +51,17 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
|||||||
const labels = row.labels ? row.labels : {};
|
const labels = row.labels ? row.labels : {};
|
||||||
const labelsAvailable = Object.keys(labels).length > 0;
|
const labelsAvailable = Object.keys(labels).length > 0;
|
||||||
const fieldsAndLinks = getAllFields(row, getFieldLinks);
|
const fieldsAndLinks = getAllFields(row, getFieldLinks);
|
||||||
const links = fieldsAndLinks.filter((f) => f.links?.length).sort();
|
let fieldsWithLinks = fieldsAndLinks.filter((f) => f.links?.length);
|
||||||
const fields = fieldsAndLinks.filter((f) => f.links?.length === 0).sort();
|
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 fieldsAvailable = fields && fields.length > 0;
|
const fieldsAvailable = fields && fields.length > 0;
|
||||||
const linksAvailable = links && links.length > 0;
|
const fieldsWithLinksAvailable =
|
||||||
|
(displayedFieldsWithLinks && displayedFieldsWithLinks.length > 0) ||
|
||||||
|
(fieldsWithLinksFromVariableMap && fieldsWithLinksFromVariableMap.length > 0);
|
||||||
|
|
||||||
// If logs with error, we are not showing the level color
|
// If logs with error, we are not showing the level color
|
||||||
const levelClassName = hasError
|
const levelClassName = hasError
|
||||||
@ -78,13 +85,13 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
|||||||
)}
|
)}
|
||||||
{Object.keys(labels)
|
{Object.keys(labels)
|
||||||
.sort()
|
.sort()
|
||||||
.map((key) => {
|
.map((key, i) => {
|
||||||
const value = labels[key];
|
const value = labels[key];
|
||||||
return (
|
return (
|
||||||
<LogDetailsRow
|
<LogDetailsRow
|
||||||
key={`${key}=${value}`}
|
key={`${key}=${value}-${i}`}
|
||||||
parsedKey={key}
|
parsedKeys={[key]}
|
||||||
parsedValue={value}
|
parsedValues={[value]}
|
||||||
isLabel={true}
|
isLabel={true}
|
||||||
getStats={() => calculateLogsLabelStats(getRows(), key)}
|
getStats={() => calculateLogsLabelStats(getRows(), key)}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
@ -95,16 +102,17 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
|||||||
app={app}
|
app={app}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
displayedFields={displayedFields}
|
displayedFields={displayedFields}
|
||||||
|
disableActions={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{fields.map((field) => {
|
{fields.map((field, i) => {
|
||||||
const { key, value, fieldIndex } = field;
|
const { keys, values, fieldIndex } = field;
|
||||||
return (
|
return (
|
||||||
<LogDetailsRow
|
<LogDetailsRow
|
||||||
key={`${key}=${value}`}
|
key={`${keys[0]}=${values[0]}-${i}`}
|
||||||
parsedKey={key}
|
parsedKeys={keys}
|
||||||
parsedValue={value}
|
parsedValues={values}
|
||||||
onClickShowField={onClickShowField}
|
onClickShowField={onClickShowField}
|
||||||
onClickHideField={onClickHideField}
|
onClickHideField={onClickHideField}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
@ -114,24 +122,25 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
|||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
row={row}
|
row={row}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableActions={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{linksAvailable && (
|
{fieldsWithLinksAvailable && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={100} className={styles.logDetailsHeading} aria-label="Data Links">
|
<td colSpan={100} className={styles.logDetailsHeading} aria-label="Data Links">
|
||||||
Links
|
Links
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{links.map((field) => {
|
{displayedFieldsWithLinks.map((field, i) => {
|
||||||
const { key, value, links, fieldIndex } = field;
|
const { keys, values, links, fieldIndex } = field;
|
||||||
return (
|
return (
|
||||||
<LogDetailsRow
|
<LogDetailsRow
|
||||||
key={`${key}=${value}`}
|
key={`${keys[0]}=${values[0]}-${i}`}
|
||||||
parsedKey={key}
|
parsedKeys={keys}
|
||||||
parsedValue={value}
|
parsedValues={values}
|
||||||
links={links}
|
links={links}
|
||||||
onClickShowField={onClickShowField}
|
onClickShowField={onClickShowField}
|
||||||
onClickHideField={onClickHideField}
|
onClickHideField={onClickHideField}
|
||||||
@ -140,10 +149,31 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
|||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
row={row}
|
row={row}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableActions={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!fieldsAvailable && !labelsAvailable && !linksAvailable && (
|
{fieldsWithLinksFromVariableMap?.map((field, i) => {
|
||||||
|
const { keys, values, links, fieldIndex } = field;
|
||||||
|
return (
|
||||||
|
<LogDetailsRow
|
||||||
|
key={`${keys[0]}=${values[0]}-${i}`}
|
||||||
|
parsedKeys={keys}
|
||||||
|
parsedValues={values}
|
||||||
|
links={links}
|
||||||
|
onClickShowField={onClickShowField}
|
||||||
|
onClickHideField={onClickHideField}
|
||||||
|
getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())}
|
||||||
|
displayedFields={displayedFields}
|
||||||
|
wrapLogMessage={wrapLogMessage}
|
||||||
|
row={row}
|
||||||
|
app={app}
|
||||||
|
disableActions={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!fieldsAvailable && !labelsAvailable && !fieldsWithLinksAvailable && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={100} aria-label="No details">
|
<td colSpan={100} aria-label="No details">
|
||||||
No details available
|
No details available
|
||||||
|
@ -9,8 +9,8 @@ type Props = ComponentProps<typeof LogDetailsRow>;
|
|||||||
|
|
||||||
const setup = (propOverrides?: Partial<Props>) => {
|
const setup = (propOverrides?: Partial<Props>) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
parsedValue: '',
|
parsedValues: [''],
|
||||||
parsedKey: '',
|
parsedKeys: [''],
|
||||||
isLabel: true,
|
isLabel: true,
|
||||||
wrapLogMessage: false,
|
wrapLogMessage: false,
|
||||||
getStats: () => null,
|
getStats: () => null,
|
||||||
@ -20,6 +20,7 @@ const setup = (propOverrides?: Partial<Props>) => {
|
|||||||
onClickHideField: () => {},
|
onClickHideField: () => {},
|
||||||
displayedFields: [],
|
displayedFields: [],
|
||||||
row: {} as LogRowModel,
|
row: {} as LogRowModel,
|
||||||
|
disableActions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -40,11 +41,11 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
|
|
||||||
describe('LogDetailsRow', () => {
|
describe('LogDetailsRow', () => {
|
||||||
it('should render parsed key', () => {
|
it('should render parsed key', () => {
|
||||||
setup({ parsedKey: 'test key' });
|
setup({ parsedKeys: ['test key'] });
|
||||||
expect(screen.getByText('test key')).toBeInTheDocument();
|
expect(screen.getByText('test key')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('should render parsed value', () => {
|
it('should render parsed value', () => {
|
||||||
setup({ parsedValue: 'test value' });
|
setup({ parsedValues: ['test value'] });
|
||||||
expect(screen.getByText('test value')).toBeInTheDocument();
|
expect(screen.getByText('test value')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,8 +74,8 @@ describe('LogDetailsRow', () => {
|
|||||||
|
|
||||||
it('should render stats when stats icon is clicked', () => {
|
it('should render stats when stats icon is clicked', () => {
|
||||||
setup({
|
setup({
|
||||||
parsedKey: 'key',
|
parsedKeys: ['key'],
|
||||||
parsedValue: 'value',
|
parsedValues: ['value'],
|
||||||
getStats: () => {
|
getStats: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -13,8 +13,9 @@ import { getLogRowStyles } from './getLogRowStyles';
|
|||||||
//Components
|
//Components
|
||||||
|
|
||||||
export interface Props extends Themeable2 {
|
export interface Props extends Themeable2 {
|
||||||
parsedValue: string;
|
parsedValues: string[];
|
||||||
parsedKey: string;
|
parsedKeys: string[];
|
||||||
|
disableActions: boolean;
|
||||||
wrapLogMessage?: boolean;
|
wrapLogMessage?: boolean;
|
||||||
isLabel?: boolean;
|
isLabel?: boolean;
|
||||||
onClickFilterLabel?: (key: string, value: string) => void;
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
@ -60,6 +61,9 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
adjoiningLinkButton: css`
|
||||||
|
margin-left: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
wrapLine: css`
|
wrapLine: css`
|
||||||
label: wrapLine;
|
label: wrapLine;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@ -68,8 +72,8 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => {
|
|||||||
padding: 0 ${theme.spacing(1)};
|
padding: 0 ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
logDetailsValue: css`
|
logDetailsValue: css`
|
||||||
display: table-cell;
|
display: flex;
|
||||||
vertical-align: middle;
|
align-items: center;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|
||||||
.show-on-hover {
|
.show-on-hover {
|
||||||
@ -105,9 +109,9 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showField = () => {
|
showField = () => {
|
||||||
const { onClickShowField: onClickShowDetectedField, parsedKey, row } = this.props;
|
const { onClickShowField: onClickShowDetectedField, parsedKeys, row } = this.props;
|
||||||
if (onClickShowDetectedField) {
|
if (onClickShowDetectedField) {
|
||||||
onClickShowDetectedField(parsedKey);
|
onClickShowDetectedField(parsedKeys[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
||||||
@ -118,9 +122,9 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
hideField = () => {
|
hideField = () => {
|
||||||
const { onClickHideField: onClickHideDetectedField, parsedKey, row } = this.props;
|
const { onClickHideField: onClickHideDetectedField, parsedKeys, row } = this.props;
|
||||||
if (onClickHideDetectedField) {
|
if (onClickHideDetectedField) {
|
||||||
onClickHideDetectedField(parsedKey);
|
onClickHideDetectedField(parsedKeys[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
||||||
@ -131,9 +135,9 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
filterLabel = () => {
|
filterLabel = () => {
|
||||||
const { onClickFilterLabel, parsedKey, parsedValue, row } = this.props;
|
const { onClickFilterLabel, parsedKeys, parsedValues, row } = this.props;
|
||||||
if (onClickFilterLabel) {
|
if (onClickFilterLabel) {
|
||||||
onClickFilterLabel(parsedKey, parsedValue);
|
onClickFilterLabel(parsedKeys[0], parsedValues[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
||||||
@ -144,9 +148,9 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
filterOutLabel = () => {
|
filterOutLabel = () => {
|
||||||
const { onClickFilterOutLabel, parsedKey, parsedValue, row } = this.props;
|
const { onClickFilterOutLabel, parsedKeys, parsedValues, row } = this.props;
|
||||||
if (onClickFilterOutLabel) {
|
if (onClickFilterOutLabel) {
|
||||||
onClickFilterOutLabel(parsedKey, parsedValue);
|
onClickFilterOutLabel(parsedKeys[0], parsedValues[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
||||||
@ -190,25 +194,68 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateClipboardButton(val: string) {
|
||||||
|
const { theme } = this.props;
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('show-on-hover', styles.copyButton)}>
|
||||||
|
<ClipboardButton
|
||||||
|
getText={() => val}
|
||||||
|
title="Copy value to clipboard"
|
||||||
|
fill="text"
|
||||||
|
variant="secondary"
|
||||||
|
icon="copy"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateMultiVal(value: string[], showCopy?: boolean) {
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{value?.map((val, i) => {
|
||||||
|
return (
|
||||||
|
<tr key={`${val}-${i}`}>
|
||||||
|
<td>
|
||||||
|
{val}
|
||||||
|
{showCopy && val !== '' && this.generateClipboardButton(val)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
parsedKey,
|
parsedKeys,
|
||||||
parsedValue,
|
parsedValues,
|
||||||
isLabel,
|
isLabel,
|
||||||
links,
|
links,
|
||||||
displayedFields,
|
displayedFields,
|
||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
onClickFilterLabel,
|
onClickFilterLabel,
|
||||||
onClickFilterOutLabel,
|
onClickFilterOutLabel,
|
||||||
|
disableActions,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const style = getLogRowStyles(theme);
|
const style = getLogRowStyles(theme);
|
||||||
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
|
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 toggleFieldButton =
|
const toggleFieldButton =
|
||||||
displayedFields && displayedFields.includes(parsedKey) ? (
|
displayedFields && parsedKeys != null && displayedFields.includes(parsedKeys[0]) ? (
|
||||||
<IconButton variant="primary" tooltip="Hide this field" name="eye" onClick={this.hideField} />
|
<IconButton variant="primary" tooltip="Hide this field" name="eye" onClick={this.hideField} />
|
||||||
) : (
|
) : (
|
||||||
<IconButton tooltip="Show this field instead of the message" name="eye" onClick={this.showField} />
|
<IconButton tooltip="Show this field instead of the message" name="eye" onClick={this.showField} />
|
||||||
@ -225,44 +272,37 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
{hasFilteringFunctionality && (
|
{hasFilteringFunctionality && (
|
||||||
<IconButton name="search-minus" tooltip="Filter out value" onClick={this.filterOutLabel} />
|
<IconButton name="search-minus" tooltip="Filter out value" onClick={this.filterOutLabel} />
|
||||||
)}
|
)}
|
||||||
{displayedFields && toggleFieldButton}
|
{!disableActions && displayedFields && toggleFieldButton}
|
||||||
|
{!disableActions && (
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={showFieldsStats ? 'primary' : 'secondary'}
|
variant={showFieldsStats ? 'primary' : 'secondary'}
|
||||||
name="signal"
|
name="signal"
|
||||||
tooltip="Ad-hoc statistics"
|
tooltip="Ad-hoc statistics"
|
||||||
className="stats-button"
|
className="stats-button"
|
||||||
|
disabled={!singleKey}
|
||||||
onClick={this.showStats}
|
onClick={this.showStats}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Key - value columns */}
|
{/* Key - value columns */}
|
||||||
<td className={style.logDetailsLabel}>{parsedKey}</td>
|
<td className={style.logDetailsLabel}>{singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)}</td>
|
||||||
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
|
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
|
||||||
<div className={styles.logDetailsValue}>
|
<div className={styles.logDetailsValue}>
|
||||||
{parsedValue}
|
{singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)}
|
||||||
|
{singleVal && this.generateClipboardButton(parsedValues[0])}
|
||||||
<div className={cx('show-on-hover', styles.copyButton)}>
|
<div className={cx((singleVal || isMultiParsedValueWithNoContent) && styles.adjoiningLinkButton)}>
|
||||||
<ClipboardButton
|
{links?.map((link, i) => (
|
||||||
getText={() => parsedValue}
|
<span key={`${link.title}-${i}`}>
|
||||||
title="Copy value to clipboard"
|
|
||||||
fill="text"
|
|
||||||
variant="secondary"
|
|
||||||
icon="copy"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{links?.map((link) => (
|
|
||||||
<span key={link.title}>
|
|
||||||
|
|
||||||
<DataLinkButton link={link} />
|
<DataLinkButton link={link} />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{showFieldsStats && (
|
{showFieldsStats && singleKey && singleVal && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -276,8 +316,8 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
<div className={styles.logDetailsStats}>
|
<div className={styles.logDetailsStats}>
|
||||||
<LogLabelStats
|
<LogLabelStats
|
||||||
stats={fieldStats!}
|
stats={fieldStats!}
|
||||||
label={parsedKey}
|
label={parsedKeys[0]}
|
||||||
value={parsedValue}
|
value={parsedValues[0]}
|
||||||
rowCount={fieldCount}
|
rowCount={fieldCount}
|
||||||
isLabel={isLabel}
|
isLabel={isLabel}
|
||||||
/>
|
/>
|
||||||
|
@ -22,16 +22,16 @@ class UnThemedLogRowMessageDisplayedFields extends PureComponent<Props> {
|
|||||||
: css`
|
: css`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
// only single key/value rows are filterable, so we only need the first field key for filtering
|
||||||
const line = showDetectedFields
|
const line = showDetectedFields
|
||||||
.map((parsedKey) => {
|
.map((parsedKey) => {
|
||||||
const field = fields.find((field) => {
|
const field = fields.find((field) => {
|
||||||
const { key } = field;
|
const { keys } = field;
|
||||||
return key === parsedKey;
|
return keys[0] === parsedKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (field !== undefined && field !== null) {
|
if (field !== undefined && field !== null) {
|
||||||
return `${parsedKey}=${field.value}`;
|
return `${parsedKey}=${field.values}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
|
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data';
|
import { ArrayVector, FieldType, MutableDataFrame } from '@grafana/data';
|
||||||
|
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
import { createLogRow } from './__mocks__/logRow';
|
import { createLogRow } from './__mocks__/logRow';
|
||||||
import { getAllFields } from './logParser';
|
import { getAllFields, createLogLineLinks, FieldDef } from './logParser';
|
||||||
|
|
||||||
describe('getAllFields', () => {
|
describe('logParser', () => {
|
||||||
|
describe('getAllFields', () => {
|
||||||
it('should filter out field with labels name and other type', () => {
|
it('should filter out field with labels name and other type', () => {
|
||||||
const logRow = createLogRow({
|
const logRow = createLogRow({
|
||||||
entryFieldIndex: 10,
|
entryFieldIndex: 10,
|
||||||
@ -23,7 +25,7 @@ describe('getAllFields', () => {
|
|||||||
|
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(1);
|
expect(fields.length).toBe(1);
|
||||||
expect(fields.find((field) => field.key === 'labels')).toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === 'labels')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not filter out field with labels name and string type', () => {
|
it('should not filter out field with labels name and string type', () => {
|
||||||
@ -44,7 +46,7 @@ describe('getAllFields', () => {
|
|||||||
});
|
});
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(2);
|
expect(fields.length).toBe(2);
|
||||||
expect(fields.find((field) => field.key === 'labels')).not.toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === 'labels')).not.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out field with id name', () => {
|
it('should filter out field with id name', () => {
|
||||||
@ -66,42 +68,7 @@ describe('getAllFields', () => {
|
|||||||
|
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(1);
|
expect(fields.length).toBe(1);
|
||||||
expect(fields.find((field) => field.key === 'id')).toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === 'id')).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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out field with config hidden field', () => {
|
it('should filter out field with config hidden field', () => {
|
||||||
@ -121,7 +88,7 @@ describe('getAllFields', () => {
|
|||||||
|
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(0);
|
expect(fields.length).toBe(0);
|
||||||
expect(fields.find((field) => field.key === testField.name)).toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === testField.name)).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out field with null values', () => {
|
it('should filter out field with null values', () => {
|
||||||
@ -135,7 +102,7 @@ describe('getAllFields', () => {
|
|||||||
|
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(0);
|
expect(fields.length).toBe(0);
|
||||||
expect(fields.find((field) => field.key === testFieldWithNullValue.name)).toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === testFieldWithNullValue.name)).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not filter out field with string values', () => {
|
it('should not filter out field with string values', () => {
|
||||||
@ -149,7 +116,97 @@ describe('getAllFields', () => {
|
|||||||
|
|
||||||
const fields = getAllFields(logRow);
|
const fields = getAllFields(logRow);
|
||||||
expect(fields.length).toBe(1);
|
expect(fields.length).toBe(1);
|
||||||
expect(fields.find((field) => field.key === testStringField.name)).not.toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === testStringField.name)).not.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 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
|
|
||||||
import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data';
|
import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data';
|
||||||
|
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
type FieldDef = {
|
export type FieldDef = {
|
||||||
key: string;
|
keys: string[];
|
||||||
value: string;
|
values: string[];
|
||||||
links?: Array<LinkModel<Field>>;
|
links?: Array<LinkModel<Field>> | ExploreFieldLinkModel[];
|
||||||
fieldIndex: number;
|
fieldIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,7 +17,11 @@ type FieldDef = {
|
|||||||
export const getAllFields = memoizeOne(
|
export const getAllFields = memoizeOne(
|
||||||
(
|
(
|
||||||
row: LogRowModel,
|
row: LogRowModel,
|
||||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
|
getFieldLinks?: (
|
||||||
|
field: Field,
|
||||||
|
rowIndex: number,
|
||||||
|
dataFrame: DataFrame
|
||||||
|
) => Array<LinkModel<Field>> | ExploreFieldLinkModel[]
|
||||||
) => {
|
) => {
|
||||||
const dataframeFields = getDataframeFields(row, getFieldLinks);
|
const dataframeFields = getDataframeFields(row, getFieldLinks);
|
||||||
|
|
||||||
@ -24,6 +29,31 @@ 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
|
* creates fields from the dataframe-fields, adding data-links, when field.config.links exists
|
||||||
*/
|
*/
|
||||||
@ -38,8 +68,8 @@ export const getDataframeFields = memoizeOne(
|
|||||||
.map((field) => {
|
.map((field) => {
|
||||||
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
|
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
|
||||||
return {
|
return {
|
||||||
key: field.name,
|
keys: [field.name],
|
||||||
value: field.values.get(row.rowIndex).toString(),
|
values: [field.values.get(row.rowIndex).toString()],
|
||||||
links: links,
|
links: links,
|
||||||
fieldIndex: field.index,
|
fieldIndex: field.index,
|
||||||
};
|
};
|
||||||
@ -57,10 +87,6 @@ function shouldRemoveField(field: Field, index: number, row: LogRowModel) {
|
|||||||
if (field.name === 'id' || field.name === 'tsNs') {
|
if (field.name === 'id' || field.name === 'tsNs') {
|
||||||
return true;
|
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);
|
const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time);
|
||||||
if (
|
if (
|
||||||
field.name === firstTimeField?.name &&
|
field.name === firstTimeField?.name &&
|
||||||
|
@ -135,7 +135,8 @@ export const escapeUnescapedString = (string: string) =>
|
|||||||
export function logRowsToReadableJson(logs: LogRowModel[]) {
|
export function logRowsToReadableJson(logs: LogRowModel[]) {
|
||||||
return logs.map((log) => {
|
return logs.map((log) => {
|
||||||
const fields = getDataframeFields(log).reduce<Record<string, string>>((acc, field) => {
|
const fields = getDataframeFields(log).reduce<Record<string, string>>((acc, field) => {
|
||||||
acc[field.key] = field.value;
|
const key = field.keys[0];
|
||||||
|
acc[key] = field.values[0];
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
@ -40,6 +40,21 @@ export class TemplateSrvMock implements TemplateSrv {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllVariablesInTarget(target: string, scopedVars: ScopedVars): Record<string, string> {
|
||||||
|
const regexp = new RegExp(this.regex);
|
||||||
|
const values: Record<string, string> = {};
|
||||||
|
|
||||||
|
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) {
|
getVariableName(expression: string) {
|
||||||
this.regex.lastIndex = 0;
|
this.regex.lastIndex = 0;
|
||||||
const match = this.regex.exec(expression);
|
const match = this.regex.exec(expression);
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
AdHocVariableFilter,
|
AdHocVariableFilter,
|
||||||
AdHocVariableModel,
|
AdHocVariableModel,
|
||||||
TypedVariableModel,
|
TypedVariableModel,
|
||||||
|
VariableMap,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
||||||
import { sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes';
|
import { sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes';
|
||||||
@ -347,6 +348,51 @@ 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) {
|
isAllValue(value: any) {
|
||||||
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
|
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
|
||||||
}
|
}
|
||||||
|
@ -190,6 +190,8 @@ describe('containsVariable', () => {
|
|||||||
${'$var'} | ${true}
|
${'$var'} | ${true}
|
||||||
${{ thing1: '${var}' }} | ${true}
|
${{ thing1: '${var}' }} | ${true}
|
||||||
${{ thing1: '${var:fmt}' }} | ${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]]'] }} | ${true}
|
${{ thing1: ['1', '[[var]]'] }} | ${true}
|
||||||
${{ thing1: ['1', '[[var:fmt]]'] }} | ${true}
|
${{ thing1: ['1', '[[var:fmt]]'] }} | ${true}
|
||||||
|
@ -16,9 +16,10 @@ import { QueryVariableModel, TransactionStatus, VariableModel, VariableRefresh,
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* This regex matches 3 types of variable reference with an optional format specifier
|
* 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+) $var1
|
||||||
* \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
* \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
||||||
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
|
* \${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?} ${var3} or ${var3.fieldPath} or ${var3:fmt3} (or ${var3.fieldPath:fmt3} but that is not a separate capture group)
|
||||||
*/
|
*/
|
||||||
export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
|
export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
|
||||||
|
|
||||||
|
@ -38,6 +38,9 @@ describe('DebugSection', () => {
|
|||||||
getVariables() {
|
getVariables() {
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
getAllVariablesInTarget(target, scopedVars) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
containsTemplate() {
|
containsTemplate() {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -21,6 +21,7 @@ describe('ZipkinDatasource', () => {
|
|||||||
replace: jest.fn(),
|
replace: jest.fn(),
|
||||||
getVariables: jest.fn(),
|
getVariables: jest.fn(),
|
||||||
containsTemplate: jest.fn(),
|
containsTemplate: jest.fn(),
|
||||||
|
getAllVariablesInTarget: jest.fn(),
|
||||||
updateTimeRange: jest.fn(),
|
updateTimeRange: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user