Canvas: Element data links refactor (#90636)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan
2024-07-25 06:13:21 -06:00
committed by GitHub
parent d3061ab61a
commit 0a870e6a88
28 changed files with 113 additions and 83 deletions

View File

@@ -190,12 +190,12 @@ You can configure a canvas data link to open with a single click on the element.
1. Click the element to which you want to add the data link. 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. 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. Scroll down to the **Data links** section and expand it.
1. Toggle the **One-click** switch in the element's data links section. 1. In the **One-click** section, choose **Link**.
1. Disable inline editing. 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. 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" >}} {{< video-embed src="/media/docs/grafana/panels-visualizations/canvas-one-click-datalink-.mp4" >}}
## Panel options ## Panel options

View File

@@ -479,7 +479,6 @@ export const getLinksSupplier =
href, href,
title: replaceVariables(link.title || '', dataLinkScopedVars), title: replaceVariables(link.title || '', dataLinkScopedVars),
target: link.targetBlank ? '_blank' : undefined, target: link.targetBlank ? '_blank' : undefined,
sortIndex: link.sortIndex,
onClick: (evt: MouseEvent, origin: Field) => { onClick: (evt: MouseEvent, origin: Field) => {
link.onClick!({ link.onClick!({
origin: origin ?? field, origin: origin ?? field,
@@ -495,7 +494,6 @@ export const getLinksSupplier =
title: replaceVariables(link.title || '', dataLinkScopedVars), title: replaceVariables(link.title || '', dataLinkScopedVars),
target: link.targetBlank ? '_blank' : undefined, target: link.targetBlank ? '_blank' : undefined,
origin: field, origin: field,
sortIndex: link.sortIndex,
}; };
} }

View File

@@ -793,6 +793,7 @@ export {
VariableOrigin, VariableOrigin,
type VariableSuggestion, type VariableSuggestion,
VariableSuggestionsScope, VariableSuggestionsScope,
OneClickMode,
} from './types/dataLink'; } from './types/dataLink';
export { DataFrameType } from './types/dataFrameTypes'; export { DataFrameType } from './types/dataFrameTypes';
export { export {

View File

@@ -99,7 +99,6 @@ export interface LinkModel<T = any> {
// When a click callback exists, this is passed the raw mouse|react event // When a click callback exists, this is passed the raw mouse|react event
onClick?: (e: any, origin?: any) => void; onClick?: (e: any, origin?: any) => void;
sortIndex?: number;
} }
/** /**
@@ -130,3 +129,8 @@ export interface VariableSuggestion {
export enum VariableSuggestionsScope { export enum VariableSuggestionsScope {
Values = 'values', Values = 'values',
} }
export enum OneClickMode {
Link = 'link',
Off = 'off',
}

View File

@@ -104,7 +104,6 @@ export interface CanvasElementOptions {
connections?: Array<CanvasConnection>; connections?: Array<CanvasConnection>;
constraint?: Constraint; constraint?: Constraint;
name: string; name: string;
oneClickLinks?: boolean;
placement?: Placement; placement?: Placement;
type: string; type: string;
} }

View File

@@ -17,7 +17,7 @@ interface DataLinksInlineEditorProps {
onChange: (links: DataLink[]) => void; onChange: (links: DataLink[]) => void;
getSuggestions: () => VariableSuggestion[]; getSuggestions: () => VariableSuggestion[];
data: DataFrame[]; data: DataFrame[];
oneClickEnabled?: boolean; showOneClick?: boolean;
} }
export const DataLinksInlineEditor = ({ export const DataLinksInlineEditor = ({
@@ -25,13 +25,12 @@ export const DataLinksInlineEditor = ({
onChange, onChange,
getSuggestions, getSuggestions,
data, data,
oneClickEnabled = false, showOneClick = false,
}: DataLinksInlineEditorProps) => { }: DataLinksInlineEditorProps) => {
const [editIndex, setEditIndex] = useState<number | null>(null); const [editIndex, setEditIndex] = useState<number | null>(null);
const [isNew, setIsNew] = useState(false); const [isNew, setIsNew] = useState(false);
const [linksSafe, setLinksSafe] = useState<DataLink[]>([]); const [linksSafe, setLinksSafe] = useState<DataLink[]>([]);
links?.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0));
useEffect(() => { useEffect(() => {
setLinksSafe(links ?? []); setLinksSafe(links ?? []);
@@ -81,24 +80,20 @@ export const DataLinksInlineEditor = ({
return; return;
} }
const copy = [...linksSafe]; const update = cloneDeep(linksSafe);
const link = copy[result.source.index]; const link = update[result.source.index];
link.sortIndex = result.destination.index;
const swapLink = copy[result.destination.index]; update.splice(result.source.index, 1);
swapLink.sortIndex = result.source.index; update.splice(result.destination.index, 0, link);
copy.splice(result.source.index, 1); setLinksSafe(update);
copy.splice(result.destination.index, 0, link); onChange(update);
setLinksSafe(copy);
onChange(linksSafe);
}; };
const renderFirstLink = (linkJSX: ReactNode) => { const renderFirstLink = (linkJSX: ReactNode, key: string) => {
if (oneClickEnabled) { if (showOneClick) {
return ( return (
<div className={styles.oneClickOverlay}> <div className={styles.oneClickOverlay} key={key}>
<span className={styles.oneClickSpan}>One-click</span> <span className={styles.oneClickSpan}>One-click</span>
{linkJSX} {linkJSX}
</div> </div>
@@ -130,7 +125,7 @@ export const DataLinksInlineEditor = ({
); );
if (idx === 0) { if (idx === 0) {
return renderFirstLink(linkJSX); return renderFirstLink(linkJSX, key);
} }
return linkJSX; return linkJSX;

View File

@@ -1,6 +1,6 @@
import { ComponentType } from 'react'; import { ComponentType } from 'react';
import { DataLink, RegistryItem } from '@grafana/data'; import { DataLink, RegistryItem, OneClickMode } from '@grafana/data';
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema'; import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
@@ -33,7 +33,7 @@ export interface CanvasElementOptions<TConfig = any> {
border?: LineConfig; border?: LineConfig;
connections?: CanvasConnection[]; connections?: CanvasConnection[];
links?: DataLink[]; links?: DataLink[];
oneClickLinks?: boolean; oneClickMode?: OneClickMode;
} }
// Unit is percentage from the middle of the element // Unit is percentage from the middle of the element

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -99,6 +99,7 @@ export const cloudItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema'; import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -94,6 +94,8 @@ export const droneFrontItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}), }),
// Called when data changes // Called when data changes

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema'; import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -93,6 +93,8 @@ export const droneSideItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}), }),
// Called when data changes // Called when data changes

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema'; import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -98,6 +98,8 @@ export const droneTopItem: CanvasElementItem = {
fixed: 'transparent', fixed: 'transparent',
}, },
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}), }),
// Called when data changes // Called when data changes

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -106,6 +106,7 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { isString } from 'lodash'; import { isString } from 'lodash';
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import { LinkModel } from '@grafana/data'; import { LinkModel, OneClickMode } from '@grafana/data';
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema'; import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
@@ -80,6 +80,7 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
left: options?.placement?.left ?? 100, left: options?.placement?.left ?? 100,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -3,7 +3,13 @@ import { useCallback } from 'react';
import { useObservable } from 'react-use'; import { useObservable } from 'react-use';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data'; import {
DataFrame,
FieldNamePickerConfigSettings,
GrafanaTheme2,
OneClickMode,
StandardEditorsRegistryItem,
} from '@grafana/data';
import { TextDimensionMode } from '@grafana/schema'; import { TextDimensionMode } from '@grafana/schema';
import { usePanelContext, useStyles2 } from '@grafana/ui'; import { usePanelContext, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
@@ -175,6 +181,7 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
left: options?.placement?.left ?? 100, left: options?.placement?.left ?? 100,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -99,6 +99,7 @@ export const parallelogramItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { stylesFactory } from '@grafana/ui'; import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
@@ -72,6 +72,7 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
fixed: defaultBgColor, fixed: defaultBgColor,
}, },
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema'; import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
import config from 'app/core/config'; import config from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -83,6 +83,7 @@ export const serverItem: CanvasElementItem<ServerConfig, ServerData> = {
config: { config: {
type: ServerType.Single, type: ServerType.Single,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import { useObservable } from 'react-use'; import { useObservable } from 'react-use';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { DataFrame, GrafanaTheme2, OneClickMode } from '@grafana/data';
import { Input, usePanelContext, useStyles2 } from '@grafana/ui'; import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -148,6 +148,7 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -100,6 +100,7 @@ export const triangleItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema'; import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -90,6 +90,7 @@ export const windTurbineItem: CanvasElementItem = {
left: options?.placement?.left, left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0, rotation: options?.placement?.rotation ?? 0,
}, },
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [], links: options?.links ?? [],
}), }),

View File

@@ -1,8 +1,8 @@
import { CSSProperties } from 'react';
import * as React from 'react'; import * as React from 'react';
import { CSSProperties } from 'react';
import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types'; import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types';
import { FieldType, getLinksSupplier, LinkModel, ValueLinkConfig } from '@grafana/data'; import { FieldType, getLinksSupplier, LinkModel, OneClickMode, ValueLinkConfig } from '@grafana/data';
import { LayerElement } from 'app/core/components/Layers/types'; import { LayerElement } from 'app/core/components/Layers/types';
import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
@@ -62,6 +62,7 @@ export class ElementState implements LayerElement {
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 }; options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 };
options.background = options.background ?? { color: { fixed: 'transparent' } }; options.background = options.background ?? { color: { fixed: 'transparent' } };
options.border = options.border ?? { color: { fixed: 'dark-green' } }; options.border = options.border ?? { color: { fixed: 'dark-green' } };
options.oneClickMode = options.oneClickMode ?? OneClickMode.Off;
const scene = this.getScene(); const scene = this.getScene();
if (!options.name) { if (!options.name) {
const newName = scene?.getNextElementName(); const newName = scene?.getNextElementName();
@@ -584,13 +585,17 @@ export class ElementState implements LayerElement {
handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => { handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => {
const scene = this.getScene(); const scene = this.getScene();
if (!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) { const shouldHandleTooltip =
!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off;
if (shouldHandleTooltip) {
this.handleTooltip(event); this.handleTooltip(event);
} else if (!isSelected) { } else if (!isSelected) {
scene?.connections.handleMouseEnter(event); scene?.connections.handleMouseEnter(event);
} }
if (this.options.oneClickLinks && this.div && this.options.links && this.options.links.length > 0) { const shouldHandleOneClickLink =
this.options.oneClickMode === OneClickMode.Link && this.options.links && this.options.links.length > 0;
if (shouldHandleOneClickLink && this.div) {
const primaryDataLink = this.getPrimaryDataLink(); const primaryDataLink = this.getPrimaryDataLink();
if (primaryDataLink) { if (primaryDataLink) {
this.div.style.cursor = 'pointer'; this.div.style.cursor = 'pointer';
@@ -602,8 +607,7 @@ export class ElementState implements LayerElement {
getPrimaryDataLink = () => { getPrimaryDataLink = () => {
if (this.getLinks) { if (this.getLinks) {
const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) }); const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) });
const primaryDataLink = links.find((link: LinkModel) => link.sortIndex === 0); return links[0];
return primaryDataLink ?? links[0];
} }
return undefined; return undefined;
@@ -623,11 +627,11 @@ export class ElementState implements LayerElement {
handleMouseLeave = (event: React.MouseEvent) => { handleMouseLeave = (event: React.MouseEvent) => {
const scene = this.getScene(); const scene = this.getScene();
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) { if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off) {
scene.tooltipCallback(undefined); scene.tooltipCallback(undefined);
} }
if (this.options.oneClickLinks && this.div) { if (this.options.oneClickMode !== OneClickMode.Off && this.div) {
this.div.style.cursor = 'auto'; this.div.style.cursor = 'auto';
this.div.title = ''; this.div.title = '';
} }
@@ -635,7 +639,7 @@ export class ElementState implements LayerElement {
onElementClick = (event: React.MouseEvent) => { onElementClick = (event: React.MouseEvent) => {
// If one-click access is enabled, open the primary link // If one-click access is enabled, open the primary link
if (this.options.oneClickLinks) { if (this.options.oneClickMode === OneClickMode.Link) {
let primaryDataLink = this.getPrimaryDataLink(); let primaryDataLink = this.getPrimaryDataLink();
if (primaryDataLink) { if (primaryDataLink) {
window.open(primaryDataLink.href, primaryDataLink.target); window.open(primaryDataLink.href, primaryDataLink.target);

View File

@@ -83,9 +83,6 @@ export const CanvasTooltip = ({ scene }: Props) => {
}); });
} }
// sort element data links
links.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0));
return ( return (
<> <>
{scene.tooltip?.element && scene.tooltip.anchorPoint && ( {scene.tooltip?.element && scene.tooltip.anchorPoint && (

View File

@@ -1,16 +1,11 @@
import { StandardEditorProps, DataLink, VariableSuggestionsScope } from '@grafana/data'; import { StandardEditorProps, DataLink, VariableSuggestionsScope, OneClickMode } from '@grafana/data';
import { DataLinksInlineEditor } from '@grafana/ui'; import { DataLinksInlineEditor } from '@grafana/ui';
import { CanvasElementOptions } from 'app/features/canvas/element';
import { CanvasElementOptions } from '../../panelcfg.gen';
type Props = StandardEditorProps<DataLink[], CanvasElementOptions>; type Props = StandardEditorProps<DataLink[], CanvasElementOptions>;
export function DataLinksEditor({ value, onChange, item, context }: Props) { export function DataLinksEditor({ value, onChange, item, context }: Props) {
if (!value) { const oneClickMode = item.settings?.oneClickMode;
value = [];
}
const settings = item.settings;
return ( return (
<DataLinksInlineEditor <DataLinksInlineEditor
@@ -18,7 +13,7 @@ export function DataLinksEditor({ value, onChange, item, context }: Props) {
onChange={onChange} onChange={onChange}
getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])}
data={[]} data={[]}
oneClickEnabled={settings?.oneClickLinks} showOneClick={oneClickMode === OneClickMode.Link}
/> />
); );
} }

View File

@@ -1,5 +1,6 @@
import { get as lodashGet } from 'lodash'; import { capitalize, get as lodashGet } from 'lodash';
import { OneClickMode } from '@grafana/data';
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
import { CanvasElementOptions } from 'app/features/canvas/element'; import { CanvasElementOptions } from 'app/features/canvas/element';
import { import {
@@ -119,6 +120,20 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
optionBuilder.addBorder(builder, ctx); optionBuilder.addBorder(builder, ctx);
} }
builder.addRadio({
category: ['Data links'],
path: 'oneClickMode',
name: 'One-click',
description: 'When enabled, a single click opens the first link',
settings: {
options: [
{ value: OneClickMode.Off, label: capitalize(OneClickMode.Off) },
{ value: OneClickMode.Link, label: capitalize(OneClickMode.Link) },
],
},
defaultValue: OneClickMode.Off,
});
optionBuilder.addDataLinks(builder, ctx); optionBuilder.addDataLinks(builder, ctx);
}, },
}; };

View File

@@ -1,6 +1,6 @@
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
import { FieldType, standardEditorsRegistry } from '@grafana/data'; import { FieldType } from '@grafana/data';
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
import { ConnectionDirection } from 'app/features/canvas/element'; import { ConnectionDirection } from 'app/features/canvas/element';
import { SVGElements } from 'app/features/canvas/runtime/element'; import { SVGElements } from 'app/features/canvas/runtime/element';
@@ -209,23 +209,13 @@ export const optionBuilder: OptionSuppliers = {
}, },
addDataLinks: (builder, context) => { addDataLinks: (builder, context) => {
const category = ['Data links']; builder.addCustomEditor({
builder category: ['Data links'],
.addCustomEditor({ id: 'dataLinks',
category, path: 'links',
id: 'enableOneClick', name: 'Links',
path: 'oneClickLinks', editor: DataLinksEditor,
name: 'One-click', settings: context.options,
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,
});
}, },
}; };

View File

@@ -1,4 +1,4 @@
import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel } from '@grafana/data'; import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel, OneClickMode } from '@grafana/data';
import { CanvasElementOptions } from 'app/features/canvas/element'; import { CanvasElementOptions } from 'app/features/canvas/element';
import { Options } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
@@ -43,7 +43,8 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
} }
} }
if (parseFloat(pluginVersion) <= 11.2) { if (parseFloat(pluginVersion) <= 11.3) {
// migrate links from field name overrides to elements
for (let idx = 0; idx < panel.fieldConfig.overrides.length; idx++) { for (let idx = 0; idx < panel.fieldConfig.overrides.length; idx++) {
const override = panel.fieldConfig.overrides[idx]; const override = panel.fieldConfig.overrides[idx];
@@ -66,6 +67,17 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
} }
} }
} }
// migrate oneClickLinks to oneClickMode
const root = panel.options?.root;
if (root?.elements) {
for (const element of root.elements) {
if (element.oneClickLinks) {
element.oneClickMode = OneClickMode.Link;
delete element.oneClickLinks;
}
}
}
} }
return panel.options; return panel.options;

View File

@@ -87,7 +87,6 @@ composableKinds: PanelCfg: {
background?: BackgroundConfig background?: BackgroundConfig
border?: LineConfig border?: LineConfig
connections?: [...CanvasConnection] connections?: [...CanvasConnection]
oneClickLinks?: bool
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
Options: { Options: {

View File

@@ -102,7 +102,6 @@ export interface CanvasElementOptions {
connections?: Array<CanvasConnection>; connections?: Array<CanvasConnection>;
constraint?: Constraint; constraint?: Constraint;
name: string; name: string;
oneClickLinks?: boolean;
placement?: Placement; placement?: Placement;
type: string; type: string;
} }