From 1726567fcfe734bb3ecb7b61ff2196faf158efcf Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:25:39 -0600 Subject: [PATCH] Canvas: Restore support for field-level data links (#93708) Co-authored-by: Leon Sorokin --- .../panel/canvas/components/CanvasTooltip.tsx | 17 +++- .../plugins/panel/canvas/migrations.test.ts | 82 ++++--------------- public/app/plugins/panel/canvas/migrations.ts | 65 +-------------- public/app/plugins/panel/canvas/utils.ts | 37 ++++++++- .../app/plugins/panel/status-history/utils.ts | 4 +- 5 files changed, 69 insertions(+), 136 deletions(-) diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index 2478d8adf4d..955656b7af7 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -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> = []; - const linkLookup = new Set(); - 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(); + 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> = []; const actionLookup = new Set(); diff --git a/public/app/plugins/panel/canvas/migrations.test.ts b/public/app/plugins/panel/canvas/migrations.test.ts index e22e98b7dd5..366b27d1717 100644 --- a/public/app/plugins/panel/canvas/migrations.test.ts +++ b/public/app/plugins/panel/canvas/migrations.test.ts @@ -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'); }); }); diff --git a/public/app/plugins/panel/canvas/migrations.ts b/public/app/plugins/panel/canvas/migrations.ts index 13af6799c11..f287556a2f7 100644 --- a/public/app/plugins/panel/canvas/migrations.ts +++ b/public/app/plugins/panel/canvas/migrations.ts @@ -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 => { } 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 => { 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 = element.config; - - for (let k in cfg) { - let dim = cfg[k]; - - // todo: getFieldDisplayName? - if (dim.field === fieldName) { - element.links ??= []; - element.links.push(...linksCopy); - } - } - }); -} diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts index f2addf63e4b..bf5fb30b15f 100644 --- a/public/app/plugins/panel/canvas/utils.ts +++ b/public/app/plugins/panel/canvas/utils.ts @@ -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(); + 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]; +} diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 946424eb2ec..f4775386ce1 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -2,11 +2,13 @@ import { Field, LinkModel } from '@grafana/data'; export const getDataLinks = (field: Field, rowIdx: number) => { const links: Array> = []; - const linkLookup = new Set(); 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(); + field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => { const key = `${link.title}/${link.href}`; if (!linkLookup.has(key)) {