Commit 76ba2db4 by Ryan McKinley Committed by GitHub

DataLinks: allow using values from other fields in the same row (#21478)

parent 3f957a37
......@@ -11,6 +11,7 @@ import {
ThresholdsMode,
FieldColorMode,
ColorScheme,
TimeZone,
} from '../types';
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
import { FieldMatcher } from '../types/transformations';
......@@ -34,6 +35,7 @@ export interface ApplyFieldOverrideOptions {
fieldOptions: FieldConfigSource;
replaceVariables: InterpolateFunction;
theme: GrafanaTheme;
timeZone?: TimeZone;
autoMinMax?: boolean;
}
......@@ -164,7 +166,11 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
type,
};
// and set the display processor using it
f.display = getDisplayProcessor({ field: f, theme: options.theme });
f.display = getDisplayProcessor({
field: f,
theme: options.theme,
timeZone: options.timeZone,
});
return f;
});
......
......@@ -11,6 +11,7 @@ import { stylesFactory } from '../../themes';
export enum VariableOrigin {
Series = 'series',
Field = 'field',
Fields = 'fields',
Value = 'value',
BuiltIn = 'built-in',
Template = 'template',
......
import { toDataFrame, applyFieldOverrides, GrafanaTheme } from '@grafana/data';
import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy';
describe('getFieldDisplayValuesProxy', () => {
const data = applyFieldOverrides({
data: [
toDataFrame({
fields: [
{ name: 'Time', values: [1, 2, 3] },
{
name: 'power',
values: [100, 200, 300],
config: {
title: 'The Power',
},
},
{
name: 'Last',
values: ['a', 'b', 'c'],
},
],
}),
],
fieldOptions: {
defaults: {},
overrides: [],
},
replaceVariables: (val: string) => val,
timeZone: 'utc',
theme: {} as GrafanaTheme,
autoMinMax: true,
})[0];
it('should define all display functions', () => {
// Field display should be set
for (const field of data.fields) {
expect(field.display).toBeDefined();
}
});
it('should format the time values in UTC', () => {
// Test Proxies in general
const p = getFieldDisplayValuesProxy(data, 0);
const time = p.Time;
expect(time.numeric).toEqual(1);
expect(time.text).toEqual('1970-01-01 00:00:00');
// Should get to the same values by name or index
const time2 = p[0];
expect(time2.toString()).toEqual(time.toString());
});
it('Lookup by name, index, or title', () => {
const p = getFieldDisplayValuesProxy(data, 2);
expect(p.power.numeric).toEqual(300);
expect(p['power'].numeric).toEqual(300);
expect(p['The Power'].numeric).toEqual(300);
expect(p[1].numeric).toEqual(300);
});
it('should return undefined when missing', () => {
const p = getFieldDisplayValuesProxy(data, 0);
expect(p.xyz).toBeUndefined();
expect(p[100]).toBeUndefined();
});
});
import { DisplayValue, DataFrame, formattedValueToString, getDisplayProcessor } from '@grafana/data';
import { config } from '@grafana/runtime';
import toNumber from 'lodash/toNumber';
export function getFieldDisplayValuesProxy(frame: DataFrame, rowIndex: number): Record<string, DisplayValue> {
return new Proxy({} as Record<string, DisplayValue>, {
get: (obj: any, key: string) => {
// 1. Match the name
let field = frame.fields.find(f => key === f.name);
if (!field) {
// 2. Match the array index
const k = toNumber(key);
field = frame.fields[k];
}
if (!field) {
// 3. Match the title
field = frame.fields.find(f => key === f.config.title);
}
if (!field) {
return undefined;
}
if (!field.display) {
// Lazy load the display processor
field.display = getDisplayProcessor({
field,
theme: config.theme,
});
}
const raw = field.values.get(rowIndex);
const disp = field.display(raw);
disp.toString = () => formattedValueToString(disp);
return disp;
},
});
}
import { getLinksFromLogsField } from './linkSuppliers';
import { ArrayVector, dateTime, Field, FieldType } from '@grafana/data';
import { getLinksFromLogsField, getFieldLinksSupplier } from './linkSuppliers';
import {
ArrayVector,
dateTime,
Field,
FieldType,
toDataFrame,
applyFieldOverrides,
GrafanaTheme,
FieldDisplay,
DataFrameView,
} from '@grafana/data';
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
import { TemplateSrv } from '../../templating/template_srv';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
......@@ -58,4 +68,131 @@ describe('getLinksFromLogsField', () => {
const links = getLinksFromLogsField(field, 2);
expect(links.length).toBe(0);
});
it('links to items on the row', () => {
const data = applyFieldOverrides({
data: [
toDataFrame({
name: 'Hello Templates',
refId: 'ZZZ',
fields: [
{ name: 'Time', values: [1, 2, 3] },
{
name: 'Power',
values: [100.2000001, 200, 300],
config: {
unit: 'kW',
decimals: 3,
title: 'TheTitle',
},
},
{
name: 'Last',
values: ['a', 'b', 'c'],
config: {
links: [
{
title: 'By Name',
url: 'http://go/${__data.fields.Power}',
},
{
title: 'By Index',
url: 'http://go/${__data.fields[1]}',
},
{
title: 'By Title',
url: 'http://go/${__data.fields[TheTitle]}',
},
{
title: 'Numeric Value',
url: 'http://go/${__data.fields.Power.numeric}',
},
{
title: 'Text (no suffix)',
url: 'http://go/${__data.fields.Power.text}',
},
{
title: 'Unknown Field',
url: 'http://go/${__data.fields.XYZ}',
},
{
title: 'Data Frame name',
url: 'http://go/${__data.name}',
},
{
title: 'Data Frame refId',
url: 'http://go/${__data.refId}',
},
],
},
},
],
}),
],
fieldOptions: {
defaults: {},
overrides: [],
},
replaceVariables: (val: string) => val,
timeZone: 'utc',
theme: {} as GrafanaTheme,
autoMinMax: true,
})[0];
const rowIndex = 0;
const colIndex = data.fields.length - 1;
const field = data.fields[colIndex];
const fieldDisp: FieldDisplay = {
name: 'hello',
field: field.config,
view: new DataFrameView(data),
rowIndex,
colIndex,
display: field.display!(field.values.get(rowIndex)),
};
const supplier = getFieldLinksSupplier(fieldDisp);
const links = supplier.getLinks({}).map(m => {
return {
title: m.title,
href: m.href,
};
});
expect(links).toMatchInlineSnapshot(`
Array [
Object {
"href": "http://go/100.200 kW",
"title": "By Name",
},
Object {
"href": "http://go/100.200 kW",
"title": "By Index",
},
Object {
"href": "http://go/100.200 kW",
"title": "By Title",
},
Object {
"href": "http://go/100.2000001",
"title": "Numeric Value",
},
Object {
"href": "http://go/100.200",
"title": "Text (no suffix)",
},
Object {
"href": "http://go/\${__data.fields.XYZ}",
"title": "Unknown Field",
},
Object {
"href": "http://go/Hello Templates",
"title": "Data Frame name",
},
Object {
"href": "http://go/ZZZ",
"title": "Data Frame refId",
},
]
`);
});
});
......@@ -8,8 +8,11 @@ import {
ScopedVar,
Field,
LinkModel,
formattedValueToString,
DisplayValue,
} from '@grafana/data';
import { getLinkSrv } from './link_srv';
import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy';
interface SeriesVars {
name?: string;
......@@ -29,10 +32,17 @@ interface ValueVars {
calc?: string;
}
interface DataViewVars {
name?: string;
refId?: string;
fields?: Record<string, DisplayValue>;
}
interface DataLinkScopedVars extends ScopedVars {
__series?: ScopedVar<SeriesVars>;
__field?: ScopedVar<FieldVars>;
__value?: ScopedVar<ValueVars>;
__data?: ScopedVar<DataViewVars>;
}
/**
......@@ -71,24 +81,36 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
};
}
if (value.rowIndex) {
if (!isNaN(value.rowIndex)) {
const { timeField } = getTimeField(dataFrame);
scopedVars['__value'] = {
value: {
raw: field.values.get(value.rowIndex),
numeric: value.display.numeric,
text: value.display.text,
text: formattedValueToString(value.display),
time: timeField ? timeField.values.get(value.rowIndex) : undefined,
},
text: 'Value',
};
// Expose other values on the row
if (value.view) {
scopedVars['__data'] = {
value: {
name: dataFrame.name,
refId: dataFrame.refId,
fields: getFieldDisplayValuesProxy(dataFrame, value.rowIndex!),
},
text: 'Data',
};
}
} else {
// calculation
scopedVars['__value'] = {
value: {
raw: value.display.numeric,
numeric: value.display.numeric,
text: value.display.text,
text: formattedValueToString(value.display),
calc: value.name,
},
text: 'Value',
......
......@@ -6,7 +6,16 @@ import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
import { sanitizeUrl } from 'app/core/utils/text';
import { getConfig } from 'app/core/config';
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
import { DataLink, KeyValue, deprecationWarning, LinkModel, DataFrame, ScopedVars } from '@grafana/data';
import {
DataLink,
KeyValue,
deprecationWarning,
LinkModel,
DataFrame,
ScopedVars,
FieldType,
Field,
} from '@grafana/data';
const timeRangeVars = [
{
......@@ -110,15 +119,81 @@ const getFieldVars = (dataFrames: DataFrame[]) => {
})),
];
};
const getDataFrameVars = (dataFrames: DataFrame[]) => {
let numeric: Field = undefined;
let title: Field = undefined;
const suggestions: VariableSuggestion[] = [];
const keys: KeyValue<true> = {};
for (const df of dataFrames) {
for (const f of df.fields) {
if (keys[f.name]) {
continue;
}
suggestions.push({
value: `__data.fields[${f.name}]`,
label: `${f.name}`,
documentation: `Formatted value for ${f.name} on the same row`,
origin: VariableOrigin.Fields,
});
keys[f.name] = true;
if (!numeric && f.type === FieldType.number) {
numeric = f;
}
if (!title && f.config.title && f.config.title !== f.name) {
title = f;
}
}
}
if (suggestions.length) {
suggestions.push({
value: `__data.fields[0]`,
label: `Select by index`,
documentation: `Enter the field order`,
origin: VariableOrigin.Fields,
});
}
if (numeric) {
suggestions.push({
value: `__data.fields[${numeric.name}].numeric`,
label: `Show numeric value`,
documentation: `the numeric field value`,
origin: VariableOrigin.Fields,
});
suggestions.push({
value: `__data.fields[${numeric.name}].text`,
label: `Show text value`,
documentation: `the text value`,
origin: VariableOrigin.Fields,
});
}
if (title) {
suggestions.push({
value: `__data.fields[${title.config.title}]`,
label: `Select by title`,
documentation: `Use the title to pick the field`,
origin: VariableOrigin.Fields,
});
}
return suggestions;
};
export const getDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
const fieldVars = getFieldVars(dataFrames);
const valueTimeVar = {
value: `${DataLinkBuiltInVars.valueTime}`,
label: 'Time',
documentation: 'Time value of the clicked datapoint (in ms epoch)',
origin: VariableOrigin.Value,
};
return [...seriesVars, ...fieldVars, ...valueVars, valueTimeVar, ...getPanelLinksVariableSuggestions()];
return [
...seriesVars,
...getFieldVars(dataFrames),
...valueVars,
valueTimeVar,
...getDataFrameVars(dataFrames),
...getPanelLinksVariableSuggestions(),
];
};
export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment