Canvas: Restore support for field-level data links (#93708)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2024-09-24 20:25:39 -06:00 committed by GitHub
parent 2b94a82baa
commit 1726567fcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 69 additions and 136 deletions

View File

@ -23,7 +23,8 @@ import { CloseButton } from '@grafana/ui/src/components/uPlot/plugins/CloseButto
import { getActions, getActionsDefaultField } from 'app/features/actions/utils';
import { Scene } from 'app/features/canvas/runtime/scene';
import { getRowIndex } from '../utils';
import { getDataLinks } from '../../status-history/utils';
import { getElementFields, getRowIndex } from '../utils';
interface Props {
scene: Scene;
@ -73,11 +74,12 @@ export const CanvasTooltip = ({ scene }: Props) => {
: []),
];
// NOTE: almost identical to getDataLinks() helper
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
const elementHasLinks = (element.options.links?.length ?? 0) > 0;
if (elementHasLinks && element.getLinks) {
if ((element.options.links?.length ?? 0) > 0 && element.getLinks) {
const linkLookup = new Set<string>();
element.getLinks({ valueRowIndex: getRowIndex(element.data.field, scene) }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
@ -86,6 +88,13 @@ export const CanvasTooltip = ({ scene }: Props) => {
}
});
}
// ---------
if (scene.data?.series) {
getElementFields(scene.data?.series, element.options).forEach((field) => {
links.push(...getDataLinks(field, getRowIndex(element.data.field, scene)));
});
}
const actions: Array<ActionModel<Field>> = [];
const actionLookup = new Set<string>();

View File

@ -1,32 +1,11 @@
import { FieldConfigSource, PanelModel } from '@grafana/data';
import { FieldConfigSource, OneClickMode, PanelModel } from '@grafana/data';
import { canvasMigrationHandler } from './migrations';
describe('Canvas data links migration', () => {
describe('Canvas migration', () => {
let prevFieldConfig: FieldConfigSource;
beforeEach(() => {
prevFieldConfig = {
defaults: {},
overrides: [
{
matcher: { id: 'byName', options: 'B-series' },
properties: [
{
id: 'links',
value: [
{ title: 'Test B-series override', url: '${__series.name}' },
{ title: 'Test B-series override 2', url: '${__field.name}' },
{ title: 'Test B-series override 3', url: '${__field.labels.foo}' },
],
},
],
},
],
};
});
it('should migrate data links', () => {
it('should migrate renamed options', () => {
const panel = {
type: 'canvas',
fieldConfig: prevFieldConfig,
@ -34,55 +13,26 @@ describe('Canvas data links migration', () => {
root: {
elements: [
{
type: 'metric-value',
config: {
text: {
mode: 'field',
field: 'B-series',
fixed: '',
},
size: 20,
color: {
fixed: '#000000',
},
align: 'center',
valign: 'middle',
},
background: {
color: {
field: 'time',
fixed: '#D9D9D9',
},
},
border: {
color: {
fixed: 'dark-green',
},
},
placement: {
top: 100,
left: 100,
width: 260,
height: 50,
},
name: 'Element 1',
constraint: {
vertical: 'top',
horizontal: 'left',
},
links: [],
type: 'ellipse',
oneClickLinks: true,
actions: [
{
options: {
url: 'http://test.com',
},
},
],
},
],
},
},
pluginVersion: '11.1.0',
pluginVersion: '11.2',
} as unknown as PanelModel;
panel.options = canvasMigrationHandler(panel);
const links = panel.options.root.elements[0].links;
expect(links).toHaveLength(3);
expect(links[0].url).toBe('${__data.fields["B-series"]}');
expect(links[1].url).toBe('${__data.fields["B-series"]}');
expect(links[2].url).toBe('${__data.fields["B-series"].labels.foo}');
expect(panel.options.root.elements[0].oneClickMode).toBe(OneClickMode.Link);
expect(panel.options.root.elements[0].actions[0].fetch.url).toBe('http://test.com');
});
});

View File

@ -1,5 +1,4 @@
import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel, OneClickMode } from '@grafana/data';
import { CanvasElementOptions } from 'app/features/canvas/element';
import { PanelModel, OneClickMode } from '@grafana/data';
import { Options } from './panelcfg.gen';
@ -44,30 +43,6 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
}
if (parseFloat(pluginVersion) <= 11.3) {
// migrate links from field name overrides to elements
for (let idx = 0; idx < panel.fieldConfig.overrides.length; idx++) {
const override = panel.fieldConfig.overrides[idx];
if (override.matcher.id === FieldMatcherID.byName) {
let props: DynamicConfigValue[] = [];
// append override links to elements with dimensions mapped to same field name
for (const prop of override.properties) {
if (prop.id === 'links') {
addLinks(panel.options.root.elements, prop.value ?? [], override.matcher.options);
} else {
props.push(prop);
}
}
if (props.length > 0) {
override.properties = props;
} else {
panel.fieldConfig.overrides.splice(idx, 1);
}
}
}
const root = panel.options?.root;
if (root?.elements) {
for (const element of root.elements) {
@ -92,41 +67,3 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
return panel.options;
};
function addLinks(elements: CanvasElementOptions[], links: DataLink[], fieldName?: string) {
const varsNamesRegex = /(\${__field.name})|(\${__field.labels.*?})|(\${__series.name})/g;
const linksCopy = [...links];
linksCopy.forEach((link) => {
const isFieldOrSeries = varsNamesRegex.test(link.url);
if (isFieldOrSeries) {
link.url = link.url.replace(varsNamesRegex, (match, fieldName1, fieldLabels1, seriesName1) => {
if (fieldName1 || seriesName1) {
return '${__data.fields["' + fieldName + '"]}';
}
if (fieldLabels1) {
const labels = fieldLabels1.match(new RegExp('.labels' + '(.*)' + '}'));
return '${__data.fields["' + fieldName + '"].labels' + labels[1] + '}';
}
return match;
});
}
});
elements.forEach((element) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let cfg: Record<string, any> = element.config;
for (let k in cfg) {
let dim = cfg[k];
// todo: getFieldDisplayName?
if (dim.field === fieldName) {
element.links ??= [];
element.links.push(...linksCopy);
}
}
});
}

View File

@ -1,6 +1,7 @@
import { isNumber, isString } from 'lodash';
import { AppEvents, PluginState, SelectableValue } from '@grafana/data';
import { AppEvents, getFieldDisplayName, PluginState, SelectableValue } from '@grafana/data';
import { DataFrame, Field } from '@grafana/data/';
import appEvents from 'app/core/app_events';
import { hasAlphaPanels, config } from 'app/core/config';
import {
@ -293,3 +294,37 @@ export const getParent = (scene: Scene) => {
}
return scene.div;
};
export function getElementFields(frames: DataFrame[], opts: CanvasElementOptions) {
const fields = new Set<Field>();
const cfg = opts.config ?? {};
frames.forEach((frame) => {
frame.fields.forEach((field) => {
const name = getFieldDisplayName(field, frame, frames);
// (intentional fall-through)
switch (name) {
// General element config
case opts.background?.color?.field:
case opts.background?.image?.field:
case opts.border?.color?.field:
// Text config
case cfg.text?.field:
case cfg.color?.field:
// Icon config
case cfg.path?.field:
case cfg.fill?.field:
// Server config
case cfg.blinkRate?.field:
case cfg.statusColor?.field:
case cfg.bulbColor?.field:
// Wind turbine config (maybe remove / not support this?)
case cfg.rpm?.field:
fields.add(field);
}
});
});
return [...fields];
}

View File

@ -2,11 +2,13 @@ import { Field, LinkModel } from '@grafana/data';
export const getDataLinks = (field: Field, rowIdx: number) => {
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
if ((field.config.links?.length ?? 0) > 0 && field.getLinks != null) {
const v = field.values[rowIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
const linkLookup = new Set<string>();
field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {