diff --git a/.betterer.results b/.betterer.results index 29a7fcff218..f80d11894df 100644 --- a/.betterer.results +++ b/.betterer.results @@ -601,7 +601,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] diff --git a/docs/sources/panels-visualizations/configure-data-links/index.md b/docs/sources/panels-visualizations/configure-data-links/index.md index 1b454b46848..7a32d801c33 100644 --- a/docs/sources/panels-visualizations/configure-data-links/index.md +++ b/docs/sources/panels-visualizations/configure-data-links/index.md @@ -248,7 +248,9 @@ If you want to add all of the current dashboard's variables to the URL, then use To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. -1. If you want the link to open in a new tab, then toggle the **Open in a new tab** switch. +1. If you want the link to open in a new tab, toggle the **Open in a new tab** switch. 1. Click **Save** to save changes and close the dialog box. 1. Click **Save dashboard**. 1. Click **Back to dashboard** and then **Exit edit**. + +If you add multiple data links, you can control the order in which they appear in the visualization. To do this, click and drag the data link to the desired position. diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 943281c9ad6..8ed73209b8d 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -166,25 +166,36 @@ The inline editing toggle lets you lock or unlock the canvas. When turned off th Canvases support [data links](ref:data-links) for all elements except drone and button elements. You can add a data link by following these steps: -1. Set an element to be tied to a field value. -1. Turn off the inline editing toggle. -1. Create an override for **Fields with name** and select the element field name from the list. -1. Click the **+ Add override property** button. -1. Select `Datalinks > Datalinks` from the list. -1. Click **+Add link** add a title and URL for the data link. -1. Hover over the element to display the data link tooltip. -1. Click on the element to be able to open the data link. +1. Enable inline editing. +1. Click the element you to which you want to add the data link. +1. In either the inline editor or panel editor, expand the **Selected element** editor. +1. Scroll down to the **Data links** section and expand it. +1. Click **Add link**. +1. In the dialog box that opens, enter a **Title**. This is a human-readable label for the link, which will be displayed in the UI. +1. Enter the **URL** or variable to which you want to link. -If multiple elements use the same field name, and you want to control which elements display the data link, you can create a unique field name using the [add field from calculation transform](ref:add-field-from-calculation-transform). The alias you create in the transformation will appear as a field you can use with an element. + To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. -1. In the panel editor for the canvas, click the **Transform** tab. -1. Select **Add field from calculation** from the list of transformations, or click **+ Add transformation** to display the list first. -1. Choose **Reduce row** from the dropdown and click the field name that you want to use for the element. -1. Select **All Values** from the **Calculation** dropdown. -1. Add an alias for the field name. -1. Reference the new unique field alias to create the element and field override. +1. If you want the link to open in a new tab, toggle the **Open in a new tab** switch. +1. Click **Save** to save changes and close the dialog box. +1. Disable inline editing. -{{< video-embed src="/media/docs/grafana/canvas-data-links-9-4-0.mp4" max-width="750px" caption="Data links demo" >}} +If you add multiple data links, you can control the order in which they appear in the visualization. To do this, click and drag the data link to the desired position. + +#### One-click data link + +You can configure a canvas data link to open with a single click on the element. To enable this feature, follow these steps: + +1. Enable inline editing. +1. Click the element to which you want to add the data link. +1. In either the inline editor or panel editor, expand the **Selected element** editor. +1. Scroll down to the **Data links** section and expand it. +1. Toggle the **One-click** switch in the element's data links section. +1. Disable inline editing. + +The first data link in the list will be configured as your one-click data link. If you want to change the one-click data link, simply drag the desired data link to the top of the list. + +{{< video-embed src="/media/docs/grafana/panels-visualizations/canvas-one-click-data-link.mp4" >}} ## Panel options diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 549599d81b4..d450dd0390c 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -479,6 +479,7 @@ export const getLinksSupplier = href, title: replaceVariables(link.title || '', dataLinkScopedVars), target: link.targetBlank ? '_blank' : undefined, + sortIndex: link.sortIndex, onClick: (evt: MouseEvent, origin: Field) => { link.onClick!({ origin: origin ?? field, @@ -494,6 +495,7 @@ export const getLinksSupplier = title: replaceVariables(link.title || '', dataLinkScopedVars), target: link.targetBlank ? '_blank' : undefined, origin: field, + sortIndex: link.sortIndex, }; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index d577b5e1a97..0bf2b85b401 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -51,6 +51,7 @@ export interface DataLink { internal?: InternalDataLink; origin?: DataLinkConfigOrigin; + sortIndex?: number; } /** @@ -98,6 +99,7 @@ export interface LinkModel { // When a click callback exists, this is passed the raw mouse|react event onClick?: (e: any, origin?: any) => void; + sortIndex?: number; } /** diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 955ada22eb7..ceaf847dfde 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -104,6 +104,7 @@ export interface CanvasElementOptions { connections?: Array; constraint?: Constraint; name: string; + oneClickLinks?: boolean; placement?: Placement; type: string; } diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 60d34096401..824f6a6451f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -54,6 +54,7 @@ "@grafana/e2e-selectors": "11.2.0-pre", "@grafana/faro-web-sdk": "^1.3.6", "@grafana/schema": "11.2.0-pre", + "@hello-pangea/dnd": "16.6.0", "@leeoniya/ufuzzy": "1.0.14", "@monaco-editor/react": "4.6.0", "@popperjs/core": "2.11.8", diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index 509c65d1dc2..a909db615da 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; +import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; import { cloneDeep } from 'lodash'; -import { useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { DataFrame, DataLink, GrafanaTheme2, VariableSuggestion } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -import { Button } from '../../Button/Button'; +import { Button } from '../../Button'; import { Modal } from '../../Modal/Modal'; import { DataLinkEditorModalContent } from './DataLinkEditorModalContent'; @@ -16,14 +17,27 @@ interface DataLinksInlineEditorProps { onChange: (links: DataLink[]) => void; getSuggestions: () => VariableSuggestion[]; data: DataFrame[]; + oneClickEnabled?: boolean; } -export const DataLinksInlineEditor = ({ links, onChange, getSuggestions, data }: DataLinksInlineEditorProps) => { +export const DataLinksInlineEditor = ({ + links, + onChange, + getSuggestions, + data, + oneClickEnabled = false, +}: DataLinksInlineEditorProps) => { const [editIndex, setEditIndex] = useState(null); const [isNew, setIsNew] = useState(false); + const [linksSafe, setLinksSafe] = useState([]); + links?.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); + + useEffect(() => { + setLinksSafe(links ?? []); + }, [links]); + const styles = useStyles2(getDataLinksInlineEditorStyles); - const linksSafe: DataLink[] = links ?? []; const isEditing = editIndex !== null; const onDataLinkChange = (index: number, link: DataLink) => { @@ -62,25 +76,70 @@ export const DataLinksInlineEditor = ({ links, onChange, getSuggestions, data }: onChange(update); }; + const onDragEnd = (result: DropResult) => { + if (!links || !result.destination) { + return; + } + + const copy = [...linksSafe]; + const link = copy[result.source.index]; + link.sortIndex = result.destination.index; + + const swapLink = copy[result.destination.index]; + swapLink.sortIndex = result.source.index; + + copy.splice(result.source.index, 1); + copy.splice(result.destination.index, 0, link); + + setLinksSafe(copy); + onChange(linksSafe); + }; + + const renderFirstLink = (linkJSX: ReactNode) => { + if (oneClickEnabled) { + return ( +
+ One-click + {linkJSX} +
+ ); + } + return linkJSX; + }; + return ( <> - {linksSafe.length > 0 && ( -
- {linksSafe.map((l, i) => { - return ( - setEditIndex(i)} - onRemove={() => onDataLinkRemove(i)} - data={data} - /> - ); - })} -
- )} + + + {(provided) => ( +
+ {linksSafe.map((link, idx) => { + const key = `${link.title}/${idx}`; + + const linkJSX = ( + setEditIndex(idx)} + onRemove={() => onDataLinkRemove(idx)} + data={data} + itemKey={key} + /> + ); + + if (idx === 0) { + return renderFirstLink(linkJSX); + } + + return linkJSX; + })} + {provided.placeholder} +
+ )} +
+
{isEditing && editIndex !== null && ( ({ wrapper: css({ marginBottom: theme.spacing(2), }), + oneClickOverlay: css({ + height: 'auto', + border: `1px dashed ${theme.colors.border.medium}`, + paddingBottom: 10, + fontSize: 10, + color: theme.colors.text.link, + }), + oneClickSpan: css({ + padding: 10, + // Negates the padding on the span from moving the underlying link + marginBottom: -10, + display: 'inline-block', + }), }); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx index bca3ab2f072..1ab1c27d4cd 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx @@ -1,3 +1,4 @@ +import { DragDropContext, Droppable } from '@hello-pangea/dnd'; import { render, screen } from '@testing-library/react'; import { DataLinksListItem, DataLinksListItemProps } from './DataLinksListItem'; @@ -17,10 +18,23 @@ function setupTestContext(options: Partial) { onChange: jest.fn(), onEdit: jest.fn(), onRemove: jest.fn(), + itemKey: 'itemKey', }; + const onDragEnd = jest.fn(); + const props = { ...defaults, ...options }; - const { rerender } = render(); + const { rerender } = render( + + + {(provided) => ( +
+ +
+ )} +
+
+ ); return { rerender, props }; } diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 705347567a6..35e828ecdd9 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -1,10 +1,12 @@ import { css, cx } from '@emotion/css'; +import { Draggable } from '@hello-pangea/dnd'; import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { isCompactUrl } from '../../../utils/dataLinks'; import { FieldValidationMessage } from '../../Forms/FieldValidationMessage'; +import { Icon } from '../../Icon/Icon'; import { IconButton } from '../../IconButton/IconButton'; export interface DataLinksListItemProps { @@ -15,9 +17,10 @@ export interface DataLinksListItemProps { onEdit: () => void; onRemove: () => void; isEditing?: boolean; + itemKey: string; } -export const DataLinksListItem = ({ link, onEdit, onRemove }: DataLinksListItemProps) => { +export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { const styles = useStyles2(getDataLinkListItemStyles); const { title = '', url = '' } = link; @@ -27,50 +30,64 @@ export const DataLinksListItem = ({ link, onEdit, onRemove }: DataLinksListItemP const isCompactExploreUrl = isCompactUrl(url); return ( -
-
-
- {hasTitle ? title : 'Data link title not provided'} -
-
- - -
-
-
- {hasUrl ? url : 'Data link url not provided'} -
- {isCompactExploreUrl && ( - Explore data link may not work in the future. Please edit. + + {(provided) => ( + <> +
+
+ +
+ +
+
+ {hasTitle ? title : 'Data link title not provided'} +
+
+ {hasUrl ? url : 'Data link url not provided'} +
+ {isCompactExploreUrl && ( + + Explore data link may not work in the future. Please edit. + + )} +
+
+ + +
+
+ )} -
+ ); }; const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { return { wrapper: css({ - marginBottom: theme.spacing(2), + display: 'flex', + flexGrow: 1, + alignItems: 'center', + justifyContent: 'space-between', width: '100%', + marginBottom: theme.spacing(2), + padding: '10px 0 0 10px', '&:last-child': { marginBottom: 0, }, + }), + linkDetails: css({ display: 'flex', flexDirection: 'column', - }), - titleWrapper: css({ - label: 'data-links-list-item-title', - justifyContent: 'space-between', - display: 'flex', - width: '100%', - alignItems: 'center', - }), - actionButtons: css({ - marginLeft: theme.spacing(1), - display: 'flex', + flexGrow: 1, }), errored: css({ color: theme.colors.error.text, @@ -92,5 +109,29 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { textOverflow: 'ellipsis', maxWidth: '90%', }), + dragRow: css({ + position: 'relative', + }), + icons: css({ + display: 'flex', + padding: 6, + alignItems: 'center', + gap: 8, + }), + dragHandle: css({ + cursor: 'grab', + // create focus ring around the whole row when the drag handle is tab-focused + // needs position: relative on the drag row to work correctly + '&:focus-visible&:after': { + bottom: 0, + content: '""', + left: 0, + position: 'absolute', + right: 0, + top: 0, + outline: `2px solid ${theme.colors.primary.main}`, + outlineOffset: '-2px', + }, + }), }; }; diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 2ebef7266ec..23cd9ee137c 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -1,6 +1,6 @@ import { ComponentType } from 'react'; -import { RegistryItem } from '@grafana/data'; +import { DataLink, RegistryItem } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema'; import { config } from 'app/core/config'; @@ -32,6 +32,8 @@ export interface CanvasElementOptions { background?: BackgroundConfig; border?: LineConfig; connections?: CanvasConnection[]; + links?: DataLink[]; + oneClickLinks?: boolean; } // Unit is percentage from the middle of the element diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx index 10b92de3c8c..cff34c95580 100644 --- a/public/app/features/canvas/elements/cloud.tsx +++ b/public/app/features/canvas/elements/cloud.tsx @@ -6,7 +6,6 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -100,6 +99,7 @@ export const cloudItem: CanvasElementItem = { left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), // Called when data changes @@ -118,8 +118,6 @@ export const cloudItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - const { background, border } = elementOptions; data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index b82c112b1ea..11de4f65e2f 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -6,7 +6,6 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -107,6 +106,7 @@ export const ellipseItem: CanvasElementItem) => { @@ -124,8 +124,6 @@ export const ellipseItem: CanvasElementItem = { left: options?.placement?.left ?? 100, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), // Called when data changes @@ -107,8 +107,6 @@ export const iconItem: CanvasElementItem = { } } - data.links = getDataLinks(dimensionContext, elementOptions, data.path); - return data; }, diff --git a/public/app/features/canvas/elements/metricValue.tsx b/public/app/features/canvas/elements/metricValue.tsx index 084ecc8b88d..0ddec47734a 100644 --- a/public/app/features/canvas/elements/metricValue.tsx +++ b/public/app/features/canvas/elements/metricValue.tsx @@ -11,7 +11,6 @@ import { frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/src/compon import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -176,6 +175,7 @@ export const metricValueItem: CanvasElementItem = { left: options?.placement?.left ?? 100, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { @@ -193,8 +193,6 @@ export const metricValueItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - return data; }, diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx index 4d4db7dfe29..3a163dd5503 100644 --- a/public/app/features/canvas/elements/parallelogram.tsx +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -6,7 +6,6 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -100,6 +99,7 @@ export const parallelogramItem: CanvasElementItem = { left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), // Called when data changes @@ -118,8 +118,6 @@ export const parallelogramItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - const { background, border } = elementOptions; data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; diff --git a/public/app/features/canvas/elements/rectangle.tsx b/public/app/features/canvas/elements/rectangle.tsx index 0daf949f5b7..08265b8deda 100644 --- a/public/app/features/canvas/elements/rectangle.tsx +++ b/public/app/features/canvas/elements/rectangle.tsx @@ -7,7 +7,6 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -73,6 +72,7 @@ export const rectangleItem: CanvasElementItem = { fixed: defaultBgColor, }, }, + links: options?.links ?? [], }), // Called when data changes @@ -91,8 +91,6 @@ export const rectangleItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - return data; }, diff --git a/public/app/features/canvas/elements/server/server.tsx b/public/app/features/canvas/elements/server/server.tsx index d84211ffe0e..b0650dbc068 100644 --- a/public/app/features/canvas/elements/server/server.tsx +++ b/public/app/features/canvas/elements/server/server.tsx @@ -5,7 +5,6 @@ import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema'; import config from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions'; import { ColorDimensionEditor, ScalarDimensionEditor } from 'app/features/dimensions/editors'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps } from '../../element'; @@ -84,6 +83,7 @@ export const serverItem: CanvasElementItem = { config: { type: ServerType.Single, }, + links: options?.links ?? [], }), // Called when data changes @@ -99,8 +99,6 @@ export const serverItem: CanvasElementItem = { type: serverConfig?.type ?? ServerType.Single, }; - data.links = getDataLinks(dimensionContext, elementOptions, data.statusColor); - return data; }, diff --git a/public/app/features/canvas/elements/text.tsx b/public/app/features/canvas/elements/text.tsx index 988b060cb99..e524163b472 100644 --- a/public/app/features/canvas/elements/text.tsx +++ b/public/app/features/canvas/elements/text.tsx @@ -10,7 +10,6 @@ import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from '../../../plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultThemeTextColor } from '../element'; import { ElementState } from '../runtime/element'; import { Align, TextConfig, TextData, VAlign } from '../types'; @@ -149,6 +148,7 @@ export const textItem: CanvasElementItem = { left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { @@ -166,8 +166,6 @@ export const textItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - return data; }, diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx index b0178142349..cf640d21a37 100644 --- a/public/app/features/canvas/elements/triangle.tsx +++ b/public/app/features/canvas/elements/triangle.tsx @@ -6,7 +6,6 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, @@ -101,6 +100,7 @@ export const triangleItem: CanvasElementItem = { left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), // Called when data changes @@ -119,8 +119,6 @@ export const triangleItem: CanvasElementItem = { data.color = dimensionContext.getColor(textConfig.color).value(); } - data.links = getDataLinks(dimensionContext, elementOptions, data.text); - const { background, border } = elementOptions; data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; diff --git a/public/app/features/canvas/elements/windTurbine.tsx b/public/app/features/canvas/elements/windTurbine.tsx index c4c1e38844d..ed3be434249 100644 --- a/public/app/features/canvas/elements/windTurbine.tsx +++ b/public/app/features/canvas/elements/windTurbine.tsx @@ -5,7 +5,6 @@ import { ScalarDimensionConfig } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; import { DimensionContext } from 'app/features/dimensions'; import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; -import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; @@ -91,6 +90,7 @@ export const windTurbineItem: CanvasElementItem = { left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, + links: options?.links ?? [], }), // Called when data changes @@ -101,8 +101,6 @@ export const windTurbineItem: CanvasElementItem = { rpm: windTurbineConfig?.rpm ? dimensionContext.getScalar(windTurbineConfig.rpm).value() : 0, }; - data.links = getDataLinks(dimensionContext, elementOptions, `${data.rpm}`); - return data; }, diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 96d099ac52f..43206d6d049 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -2,6 +2,7 @@ import { CSSProperties } from 'react'; import * as React from 'react'; import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types'; +import { FieldType, getLinksSupplier, LinkModel, ValueLinkConfig } from '@grafana/data'; import { LayerElement } from 'app/core/components/Layers/types'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { DimensionContext } from 'app/features/dimensions'; @@ -12,7 +13,7 @@ import { Placement, VerticalConstraint, } from 'app/plugins/panel/canvas/panelcfg.gen'; -import { getConnectionsByTarget, isConnectionTarget } from 'app/plugins/panel/canvas/utils'; +import { getConnectionsByTarget, getRowIndex, isConnectionTarget } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementOptions } from '../element'; import { canvasElementRegistry } from '../registry'; @@ -42,6 +43,8 @@ export class ElementState implements LayerElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; // depends on the type + getLinks?: (config: ValueLinkConfig) => LinkModel[]; + constructor( public item: CanvasElementItem, public options: CanvasElementOptions, @@ -366,6 +369,34 @@ export class ElementState implements LayerElement { this.revId++; // rerender } + const scene = this.getScene(); + const frames = scene?.data?.series; + + if (frames) { + const defaultField = { + name: 'Default field', + type: FieldType.string, + config: { links: this.options.links ?? [] }, + values: [], + }; + + this.getLinks = getLinksSupplier( + frames[0], + defaultField, + { + __dataContext: { + value: { + data: frames, + field: defaultField, + frame: frames[0], + frameIndex: 0, + }, + }, + }, + scene?.panel.props.replaceVariables! + ); + } + const { background, border } = this.options; const css: CSSProperties = {}; if (background) { @@ -552,11 +583,30 @@ export class ElementState implements LayerElement { handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => { const scene = this.getScene(); - if (!scene?.isEditingEnabled && !scene?.tooltip?.isOpen) { + + if (!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) { this.handleTooltip(event); } else if (!isSelected) { scene?.connections.handleMouseEnter(event); } + + if (this.options.oneClickLinks && this.div && this.options.links && this.options.links.length > 0) { + const primaryDataLink = this.getPrimaryDataLink(); + if (primaryDataLink) { + this.div.style.cursor = 'pointer'; + this.div.title = `Navigate to ${primaryDataLink.title === '' ? 'data link' : primaryDataLink.title}`; + } + } + }; + + getPrimaryDataLink = () => { + if (this.getLinks) { + const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) }); + const primaryDataLink = links.find((link: LinkModel) => link.sortIndex === 0); + return primaryDataLink ?? links[0]; + } + + return undefined; }; handleTooltip = (event: React.MouseEvent) => { @@ -573,14 +623,27 @@ export class ElementState implements LayerElement { handleMouseLeave = (event: React.MouseEvent) => { const scene = this.getScene(); - if (scene?.tooltipCallback && !scene?.tooltip?.isOpen) { + if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) { scene.tooltipCallback(undefined); } + + if (this.options.oneClickLinks && this.div) { + this.div.style.cursor = 'auto'; + this.div.title = ''; + } }; onElementClick = (event: React.MouseEvent) => { - this.handleTooltip(event); - this.onTooltipCallback(); + // If one-click access is enabled, open the primary link + if (this.options.oneClickLinks) { + let primaryDataLink = this.getPrimaryDataLink(); + if (primaryDataLink) { + window.open(primaryDataLink.href, primaryDataLink.target); + } + } else { + this.handleTooltip(event); + this.onTooltipCallback(); + } }; onElementKeyDown = (event: React.KeyboardEvent) => { diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 2e96e69b0c9..6784acd8636 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -281,7 +281,9 @@ export class Scene { }; render() { - const isTooltipValid = (this.tooltip?.element?.data?.links?.length ?? 0) > 0 || this.tooltip?.element?.data?.field; + const isTooltipValid = + (this.tooltip?.element?.getLinks && this.tooltip?.element?.getLinks({}).length > 0) || + this.tooltip?.element?.data?.field; const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid; const sceneDiv = ( diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index 0c7db1533ce..4142becbf4e 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -3,7 +3,14 @@ import { useDialog } from '@react-aria/dialog'; import { useOverlay } from '@react-aria/overlays'; import { createRef } from 'react'; -import { FieldType, GrafanaTheme2, formattedValueToString, getFieldDisplayName } from '@grafana/data/src'; +import { + Field, + LinkModel, + FieldType, + GrafanaTheme2, + formattedValueToString, + getFieldDisplayName, +} from '@grafana/data/src'; import { Portal, useStyles2, VizTooltipContainer } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; @@ -12,6 +19,8 @@ import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { CloseButton } from '@grafana/ui/src/components/uPlot/plugins/CloseButton'; import { Scene } from 'app/features/canvas/runtime/scene'; +import { getRowIndex } from '../utils'; + interface Props { scene: Scene; } @@ -60,6 +69,23 @@ export const CanvasTooltip = ({ scene }: Props) => { : []), ]; + const links: Array> = []; + const linkLookup = new Set(); + + const elementHasLinks = (element.options.links?.length ?? 0) > 0; + if (elementHasLinks && element.getLinks) { + element.getLinks({ valueRowIndex: getRowIndex(element.data.field, scene) }).forEach((link) => { + const key = `${link.title}/${link.href}`; + if (!linkLookup.has(key)) { + links.push(link); + linkLookup.add(key); + } + }); + } + + // sort element data links + links.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); + return ( <> {scene.tooltip?.element && scene.tooltip.anchorPoint && ( @@ -74,7 +100,7 @@ export const CanvasTooltip = ({ scene }: Props) => { {scene.tooltip.isOpen && } {element.data.text && } - + {links.length > 0 && } diff --git a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx new file mode 100644 index 00000000000..5df3052762c --- /dev/null +++ b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx @@ -0,0 +1,24 @@ +import { StandardEditorProps, DataLink, VariableSuggestionsScope } from '@grafana/data'; +import { DataLinksInlineEditor } from '@grafana/ui'; + +import { CanvasElementOptions } from '../../panelcfg.gen'; + +type Props = StandardEditorProps; + +export function DataLinksEditor({ value, onChange, item, context }: Props) { + if (!value) { + value = []; + } + + const settings = item.settings; + + return ( + (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} + data={[]} + oneClickEnabled={settings?.oneClickLinks} + /> + ); +} diff --git a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx index 09d602c4639..6133ff8bcac 100644 --- a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx @@ -118,6 +118,8 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< if (shouldAddBorderEditor) { optionBuilder.addBorder(builder, ctx); } + + optionBuilder.addDataLinks(builder, ctx); }, }; } diff --git a/public/app/plugins/panel/canvas/editor/options.ts b/public/app/plugins/panel/canvas/editor/options.ts index 0baa970f0e6..085e369eca2 100644 --- a/public/app/plugins/panel/canvas/editor/options.ts +++ b/public/app/plugins/panel/canvas/editor/options.ts @@ -1,6 +1,6 @@ import { capitalize } from 'lodash'; -import { FieldType } from '@grafana/data'; +import { FieldType, standardEditorsRegistry } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { ConnectionDirection } from 'app/features/canvas/element'; import { SVGElements } from 'app/features/canvas/runtime/element'; @@ -11,10 +11,12 @@ import { CanvasConnection, CanvasElementOptions } from '../panelcfg.gen'; import { LineStyle } from '../types'; import { LineStyleEditor } from './LineStyleEditor'; +import { DataLinksEditor } from './element/DataLinksEditor'; interface OptionSuppliers { addBackground: PanelOptionsSupplier; addBorder: PanelOptionsSupplier; + addDataLinks: PanelOptionsSupplier; addColor: PanelOptionsSupplier; addSize: PanelOptionsSupplier; addRadius: PanelOptionsSupplier; @@ -205,4 +207,25 @@ export const optionBuilder: OptionSuppliers = { defaultValue: { value: LineStyle.Solid, label: 'Solid' }, }); }, + + addDataLinks: (builder, context) => { + const category = ['Data links']; + builder + .addCustomEditor({ + category, + id: 'enableOneClick', + path: 'oneClickLinks', + name: 'One-click', + description: 'When enabled, the top link in the list below works with a single click', + editor: standardEditorsRegistry.get('boolean').editor, + }) + .addCustomEditor({ + category, + id: 'dataLinks', + path: 'links', + name: '', + editor: DataLinksEditor, + settings: context.options, + }); + }, }; diff --git a/public/app/plugins/panel/canvas/globalStyles.ts b/public/app/plugins/panel/canvas/globalStyles.ts index 29ea977d2d2..a61d39d39d5 100644 --- a/public/app/plugins/panel/canvas/globalStyles.ts +++ b/public/app/plugins/panel/canvas/globalStyles.ts @@ -111,6 +111,7 @@ export function getGlobalStyles(theme: GrafanaTheme2) { display: 'flex', alignItems: 'center', width: '16px', + height: 'auto', backgroundColor: 'transparent', backgroundRepeat: 'no-repeat', backgroundAttachment: 'scroll', diff --git a/public/app/plugins/panel/canvas/migrations.test.ts b/public/app/plugins/panel/canvas/migrations.test.ts new file mode 100644 index 00000000000..e22e98b7dd5 --- /dev/null +++ b/public/app/plugins/panel/canvas/migrations.test.ts @@ -0,0 +1,88 @@ +import { FieldConfigSource, PanelModel } from '@grafana/data'; + +import { canvasMigrationHandler } from './migrations'; + +describe('Canvas data links 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', () => { + const panel = { + type: 'canvas', + fieldConfig: prevFieldConfig, + options: { + 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: [], + }, + ], + }, + }, + pluginVersion: '11.1.0', + } 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}'); + }); +}); diff --git a/public/app/plugins/panel/canvas/migrations.ts b/public/app/plugins/panel/canvas/migrations.ts index 21e8122c884..4a189c491ee 100644 --- a/public/app/plugins/panel/canvas/migrations.ts +++ b/public/app/plugins/panel/canvas/migrations.ts @@ -1,4 +1,5 @@ -import { PanelModel } from '@grafana/data'; +import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel } from '@grafana/data'; +import { CanvasElementOptions } from 'app/features/canvas/element'; import { Options } from './panelcfg.gen'; @@ -42,5 +43,68 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial => { } } + if (parseFloat(pluginVersion) < 11.2) { + 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); + } + } + } + } + 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/module.tsx b/public/app/plugins/panel/canvas/module.tsx index b7fc9e8a5fd..19b9ce04ded 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -58,6 +58,9 @@ export const plugin = new PanelPlugin(CanvasPanel) icon: true, }, }, + [FieldConfigProperty.Links]: { + hideFromDefaults: true, + }, }, }) .setMigrationHandler(canvasMigrationHandler) diff --git a/public/app/plugins/panel/canvas/panelcfg.cue b/public/app/plugins/panel/canvas/panelcfg.cue index 6da7cebacc4..16c72f86f6a 100644 --- a/public/app/plugins/panel/canvas/panelcfg.cue +++ b/public/app/plugins/panel/canvas/panelcfg.cue @@ -87,6 +87,7 @@ composableKinds: PanelCfg: { background?: BackgroundConfig border?: LineConfig connections?: [...CanvasConnection] + oneClickLinks?: bool } @cuetsy(kind="interface") Options: { diff --git a/public/app/plugins/panel/canvas/panelcfg.gen.ts b/public/app/plugins/panel/canvas/panelcfg.gen.ts index 9706c886f6d..215f1f65162 100644 --- a/public/app/plugins/panel/canvas/panelcfg.gen.ts +++ b/public/app/plugins/panel/canvas/panelcfg.gen.ts @@ -102,6 +102,7 @@ export interface CanvasElementOptions { connections?: Array; constraint?: Constraint; name: string; + oneClickLinks?: boolean; placement?: Placement; type: string; } diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts index 964f3ce2011..f2addf63e4b 100644 --- a/public/app/plugins/panel/canvas/utils.ts +++ b/public/app/plugins/panel/canvas/utils.ts @@ -1,6 +1,6 @@ import { isNumber, isString } from 'lodash'; -import { AppEvents, Field, getFieldDisplayName, LinkModel, PluginState, SelectableValue } from '@grafana/data'; +import { AppEvents, PluginState, SelectableValue } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { hasAlphaPanels, config } from 'app/core/config'; import { @@ -14,7 +14,6 @@ import { advancedElementItems, canvasElementRegistry, defaultElementItems } from import { ElementState } from 'app/features/canvas/runtime/element'; import { FrameState } from 'app/features/canvas/runtime/frame'; import { Scene, SelectionParams } from 'app/features/canvas/runtime/scene'; -import { DimensionContext } from 'app/features/dimensions'; import { AnchorPoint, ConnectionState, LineStyle, StrokeDasharray } from './types'; @@ -105,169 +104,6 @@ export function onAddItem(sel: SelectableValue, rootLayer: FrameState | } } -/* - * Provided a given field add any matching data links - * Mutates the links object in place which is then returned by the `getDataLinks` function downstream - */ -const addDataLinkForField = ( - field: Field, - data: string | undefined, - linkLookup: Set, - links: Array> -): void => { - if (field?.getLinks) { - const disp = field.display ? field.display(data) : { text: `${data}`, numeric: +data! }; - // TODO add more control over which row index each element uses - const valueRowIndex = field.values.length - 1; - field.getLinks({ calculatedValue: disp, valueRowIndex }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); - } -}; - -// TODO: This could be refactored a fair amount, ideally the element specific config code should be owned by each element and not in this shared util file -export function getDataLinks( - dimensionContext: DimensionContext, - elementOptions: CanvasElementOptions, - data: string | undefined -): LinkModel[] { - const panelData = dimensionContext.getPanelData(); - const frames = panelData?.series; - - const links: Array> = []; - const linkLookup = new Set(); - - const elementConfig = elementOptions.config; - - frames?.forEach((frame) => { - const visibleFields = frame.fields.filter((field) => !Boolean(field.config.custom?.hideFrom?.tooltip)); - - // Text config - const isTextTiedToFieldData = - elementConfig?.text?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.text?.field); - const isTextColorTiedToFieldData = - elementConfig?.color?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.color?.field); - - // General element config - const isElementBackgroundColorTiedToFieldData = - elementOptions?.background?.color?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.background?.color?.field); - const isElementBackgroundImageTiedToFieldData = - elementOptions?.background?.image?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.background?.image?.field); - const isElementBorderColorTiedToFieldData = - elementOptions?.border?.color?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.border?.color?.field); - - // Icon config - const isIconSVGTiedToFieldData = - elementConfig?.path?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.path?.field); - const isIconColorTiedToFieldData = - elementConfig?.fill?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.fill?.field); - - // Wind turbine config (maybe remove / not support this?) - const isWindTurbineRPMTiedToFieldData = - elementConfig?.rpm?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.rpm?.field); - - // Server config - const isServerBlinkRateTiedToFieldData = - elementConfig?.blinkRate?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.blinkRate?.field); - const isServerStatusColorTiedToFieldData = - elementConfig?.statusColor?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.statusColor?.field); - const isServerBulbColorTiedToFieldData = - elementConfig?.bulbColor?.field && - visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig?.bulbColor?.field); - - if (isTextTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.text?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isTextColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.color?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isElementBackgroundColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementOptions?.background?.color?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isElementBackgroundImageTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementOptions?.background?.image?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isElementBorderColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementOptions?.border?.color?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isIconSVGTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.path?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isIconColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.fill?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isWindTurbineRPMTiedToFieldData) { - const field = visibleFields.filter((field) => getFieldDisplayName(field, frame) === elementConfig?.rpm?.field)[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isServerBlinkRateTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.blinkRate?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isServerStatusColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.statusColor?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - - if (isServerBulbColorTiedToFieldData) { - const field = visibleFields.filter( - (field) => getFieldDisplayName(field, frame) === elementConfig?.bulbColor?.field - )[0]; - addDataLinkForField(field, data, linkLookup, links); - } - }); - - return links; -} - export function isConnectionSource(element: ElementState) { return element.options.connections && element.options.connections.length > 0; } @@ -398,8 +234,8 @@ export const calculateDistance = (x1: number, y1: number, x2: number, y2: number // @TODO revisit, currently returning last row index for field export const getRowIndex = (fieldName: string | undefined, scene: Scene) => { if (fieldName) { - const series = scene.context.getPanelData()?.series[0]; - const field = series?.fields.find((f) => (f.name = fieldName)); + const series = scene.data?.series[0]; + const field = series?.fields.find((field) => field.name === fieldName); const data = field?.values; return data ? data.length - 1 : 0; } diff --git a/yarn.lock b/yarn.lock index 180c3ed8d55..ca0cf7f336a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3685,6 +3685,7 @@ __metadata: "@grafana/faro-web-sdk": "npm:^1.3.6" "@grafana/schema": "npm:11.2.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" + "@hello-pangea/dnd": "npm:16.6.0" "@leeoniya/ufuzzy": "npm:1.0.14" "@monaco-editor/react": "npm:4.6.0" "@popperjs/core": "npm:2.11.8"