mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Restore support for field-level data links (#93708)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
2b94a82baa
commit
1726567fcf
@ -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>();
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user