From 9022357fb7f5462f0b34558bcd4cdb0a0530c9c1 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:26:36 -0500 Subject: [PATCH] Canvas: Add option to disable the tooltip (#108196) --- .../visualizations/canvas/index.md | 8 ++++ .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 8 ++++ .../app/features/canvas/runtime/element.tsx | 11 ++--- public/app/features/canvas/runtime/scene.tsx | 43 +++++++++++-------- .../app/plugins/panel/canvas/CanvasPanel.tsx | 32 +++----------- .../panel/canvas/components/CanvasTooltip.tsx | 18 ++++---- public/app/plugins/panel/canvas/module.tsx | 24 ++++++++++- public/app/plugins/panel/canvas/panelcfg.cue | 5 +++ .../app/plugins/panel/canvas/panelcfg.gen.ts | 8 ++++ public/locales/en-US/grafana.json | 8 ++++ 10 files changed, 106 insertions(+), 59 deletions(-) diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 4be10911af6..a5f4712c502 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -270,6 +270,14 @@ You can enable infinite panning in a canvas when pan and zoom is enabled. This a Infinite panning is an experimental feature that may not work as expected in all scenarios. For example, elements that are not top-left constrained may experience unexpected movement when panning. {{< /admonition >}} +### Tooltip options + +The **Tooltip mode** setting controls the display of tooltips when hovering over canvas elements that are connected to data, data links, or actions. +The options are: + +- **Enabled** - Show a tooltip when the cursor hovers over an element. +- **Disabled** - Tooltips are not shown on hover. + ### Layer options The **Layer** options let you add elements to the canvas and control its appearance: 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 983129db72b..722317c115a 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 @@ -112,6 +112,10 @@ export const defaultCanvasElementOptions: Partial = { connections: [], }; +export interface CanvasTooltip { + mode: ui.TooltipDisplayMode; +} + export interface Options { /** * Enable infinite pan @@ -147,6 +151,10 @@ export interface Options { * Show all available element types */ showAdvancedTypes: boolean; + /** + * Controls tooltip options + */ + tooltip: CanvasTooltip; } export const defaultOptions: Partial = { diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 4659c2c0a84..d06c7d5caee 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -13,6 +13,7 @@ import { ActionVariableInput, } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { TooltipDisplayMode } from '@grafana/schema'; import { ConfirmModal, VariablesInputModal } from '@grafana/ui'; import { LayerElement } from 'app/core/components/Layers/types'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; @@ -619,7 +620,7 @@ export class ElementState implements LayerElement { handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => { const scene = this.getScene(); - const shouldHandleTooltip = !scene?.isEditingEnabled && !scene?.tooltip?.isOpen; + const shouldHandleTooltip = !scene?.isEditingEnabled && !scene?.tooltipPayload?.isOpen; if (shouldHandleTooltip) { this.handleTooltip(event); } else if (!isSelected) { @@ -686,7 +687,7 @@ export class ElementState implements LayerElement { handleTooltip = (event: React.MouseEvent) => { const scene = this.getScene(); - if (scene?.tooltipCallback) { + if (scene?.tooltipCallback && scene.tooltipMode !== TooltipDisplayMode.None) { const rect = this.div?.getBoundingClientRect(); scene.tooltipCallback({ anchorPoint: { x: rect?.right ?? event.pageX, y: rect?.top ?? event.pageY }, @@ -698,7 +699,7 @@ export class ElementState implements LayerElement { handleMouseLeave = (event: React.MouseEvent) => { const scene = this.getScene(); - if (scene?.tooltipCallback && !scene?.tooltip?.isOpen) { + if (scene?.tooltipCallback && !scene?.tooltipPayload?.isOpen) { scene.tooltipCallback(undefined); } @@ -744,9 +745,9 @@ export class ElementState implements LayerElement { onTooltipCallback = () => { const scene = this.getScene(); - if (scene?.tooltipCallback && scene.tooltip?.anchorPoint) { + if (scene?.tooltipCallback && scene.tooltipPayload?.anchorPoint) { scene.tooltipCallback({ - anchorPoint: { x: scene.tooltip.anchorPoint.x, y: scene.tooltip.anchorPoint.y }, + anchorPoint: { x: scene.tooltipPayload.anchorPoint.x, y: scene.tooltipPayload.anchorPoint.y }, element: this, isOpen: true, }); diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 5e54afd9be8..298b232e16b 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -13,6 +13,7 @@ import { ScalarDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, + TooltipDisplayMode, } from '@grafana/schema'; import { Portal } from '@grafana/ui'; import { config } from 'app/core/config'; @@ -27,11 +28,13 @@ import { import { CanvasContextMenu } from 'app/plugins/panel/canvas/components/CanvasContextMenu'; import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip'; import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections'; +import { Options } from 'app/plugins/panel/canvas/panelcfg.gen'; import { AnchorPoint, CanvasTooltipPayload } from 'app/plugins/panel/canvas/types'; import { getTransformInstance } from 'app/plugins/panel/canvas/utils'; import appEvents from '../../../core/app_events'; import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel'; +import { getDashboardSrv } from '../../dashboard/services/DashboardSrv'; import { CanvasFrameOptions } from '../frame'; import { DEFAULT_CANVAS_ELEMENT_CONFIG } from '../registry'; @@ -71,6 +74,7 @@ export class Scene { shouldShowAdvancedTypes?: boolean; shouldPanZoom?: boolean; shouldInfinitePan?: boolean; + tooltipMode?: TooltipDisplayMode; skipNextSelectionBroadcast = false; ignoreDataUpdate = false; panel: CanvasPanel; @@ -93,7 +97,7 @@ export class Scene { setBackgroundCallback?: (anchorPoint: AnchorPoint) => void; tooltipCallback?: (tooltip: CanvasTooltipPayload | undefined) => void; - tooltip?: CanvasTooltipPayload; + tooltipPayload?: CanvasTooltipPayload; moveableActionCallback?: (moved: boolean) => void; @@ -106,15 +110,16 @@ export class Scene { transformComponentRef: RefObject | undefined; constructor( - cfg: CanvasFrameOptions, - enableEditing: boolean, - showAdvancedTypes: boolean, - panZoom: boolean, - infinitePan: boolean, + options: Options, public onSave: (cfg: CanvasFrameOptions) => void, panel: CanvasPanel ) { - this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom, infinitePan); + // TODO: Will need to update this approach for dashboard scenes + // migration (new dashboard edit experience) + const dashboard = getDashboardSrv().getCurrent(); + const enableEditing = options.inlineEditing && dashboard?.editable; + + this.root = this.load(options, enableEditing); this.subscription = this.editModeEnabled.subscribe((open) => { if (!this.moveable || !this.isEditingEnabled) { @@ -147,15 +152,12 @@ export class Scene { return !this.byName.has(v); }; - load( - cfg: CanvasFrameOptions, - enableEditing: boolean, - showAdvancedTypes: boolean, - panZoom: boolean, - infinitePan: boolean - ) { + load(options: Options, enableEditing: boolean) { + const { root, showAdvancedTypes, panZoom, infinitePan, tooltip } = options; + const tooltipMode = tooltip?.mode ?? TooltipDisplayMode.Single; + this.root = new RootElement( - cfg ?? { + root ?? { type: 'frame', elements: [DEFAULT_CANVAS_ELEMENT_CONFIG], }, @@ -167,6 +169,7 @@ export class Scene { this.shouldShowAdvancedTypes = showAdvancedTypes; this.shouldPanZoom = panZoom; this.shouldInfinitePan = infinitePan; + this.tooltipMode = tooltipMode; setTimeout(() => { if (this.div) { @@ -283,11 +286,13 @@ export class Scene { }; render() { - const hasDataLinks = this.tooltip?.element?.getLinks && this.tooltip.element.getLinks({}).length > 0; - const hasActions = this.tooltip?.element?.options.actions && this.tooltip.element.options.actions.length > 0; + const hasDataLinks = this.tooltipPayload?.element?.getLinks && this.tooltipPayload.element.getLinks({}).length > 0; + const hasActions = + this.tooltipPayload?.element?.options.actions && this.tooltipPayload.element.options.actions.length > 0; - const isTooltipValid = hasDataLinks || hasActions || this.tooltip?.element?.data?.field; - const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid; + const isTooltipValid = hasDataLinks || hasActions || this.tooltipPayload?.element?.data?.field; + const isTooltipEnabled = this.tooltipMode !== TooltipDisplayMode.None; + const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid && isTooltipEnabled; const sceneDiv = (
diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 10f43819839..3af277aa833 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -1,5 +1,5 @@ -import { Component } from 'react'; import * as React from 'react'; +import { Component } from 'react'; import { ReplaySubject, Subscription } from 'rxjs'; import { PanelProps } from '@grafana/data'; @@ -8,7 +8,6 @@ import { PanelContext, PanelContextRoot } from '@grafana/ui'; import { CanvasFrameOptions } from 'app/features/canvas/frame'; import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; import { SetBackground } from './components/SetBackground'; @@ -63,22 +62,9 @@ export class CanvasPanel extends Component { moveableAction: false, }; - // TODO: Will need to update this approach for dashboard scenes - // migration (new dashboard edit experience) - const dashboard = getDashboardSrv().getCurrent(); - const allowEditing = this.props.options.inlineEditing && dashboard?.editable; - // Only the initial options are ever used. // later changes are all controlled by the scene - this.scene = new Scene( - this.props.options.root, - allowEditing, - this.props.options.showAdvancedTypes, - this.props.options.panZoom, - this.props.options.infinitePan, - this.onUpdateScene, - this - ); + this.scene = new Scene(this.props.options, this.onUpdateScene, this); this.scene.updateSize(props.width, props.height); this.scene.updateData(props.data); this.scene.inlineEditingCallback = this.openInlineEdit; @@ -239,12 +225,14 @@ export class CanvasPanel extends Component { this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes; const panZoomSwitched = this.props.options.panZoom !== nextProps.options.panZoom; const infinitePanSwitched = this.props.options.infinitePan !== nextProps.options.infinitePan; + const tooltipModeSwitched = this.props.options.tooltip?.mode !== nextProps.options.tooltip?.mode; if ( this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched || panZoomSwitched || - infinitePanSwitched + infinitePanSwitched || + tooltipModeSwitched ) { if (inlineEditingSwitched) { // Replace scene div to prevent selecto instance leaks @@ -252,13 +240,7 @@ export class CanvasPanel extends Component { } this.needsReload = false; - this.scene.load( - nextProps.options.root, - nextProps.options.inlineEditing, - nextProps.options.showAdvancedTypes, - nextProps.options.panZoom, - nextProps.options.infinitePan - ); + this.scene.load(nextProps.options, nextProps.options.inlineEditing); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); changed = true; @@ -294,7 +276,7 @@ export class CanvasPanel extends Component { }; tooltipCallback = (tooltip: CanvasTooltipPayload | undefined) => { - this.scene.tooltip = tooltip; + this.scene.tooltipPayload = tooltip; this.forceUpdate(); }; diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index ceb17a8ce7d..aa0e29741c5 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -36,7 +36,7 @@ export const CanvasTooltip = ({ scene }: Props) => { const styles = useStyles2(getStyles); const onClose = () => { - if (scene?.tooltipCallback && scene.tooltip) { + if (scene?.tooltipCallback && scene.tooltipPayload) { scene.tooltipCallback(undefined); } }; @@ -45,7 +45,7 @@ export const CanvasTooltip = ({ scene }: Props) => { const { overlayProps } = useOverlay({ onClose: onClose, isDismissable: true }, ref); const { dialogProps } = useDialog({}, ref); - const element = scene.tooltip?.element; + const element = scene.tooltipPayload?.element; if (!element) { return <>; } @@ -138,18 +138,18 @@ export const CanvasTooltip = ({ scene }: Props) => { return ( <> - {scene.tooltip?.element && scene.tooltip.anchorPoint && ( + {scene.tooltipPayload?.element && scene.tooltipPayload.anchorPoint && (
- {scene.tooltip.isOpen && } - - {element.data.text && } + {scene.tooltipPayload.isOpen && } + + {element.data.text && } {(links.length > 0 || actions.length > 0) && }
diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index af54dbc280d..512509841ca 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -1,6 +1,7 @@ import { FieldConfigProperty, PanelOptionsEditorBuilder, PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; +import { TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; import { FrameState } from 'app/features/canvas/runtime/frame'; import { CanvasPanel, InstanceState } from './CanvasPanel'; @@ -12,7 +13,7 @@ import { canvasMigrationHandler } from './migrations'; import { Options } from './panelcfg.gen'; export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilder) => { - const category = [t('canvas.category-canvas', 'Canvas')]; + let category = [t('canvas.category-canvas', 'Canvas')]; builder.addBooleanSwitch({ path: 'inlineEditing', name: t('canvas.name-inline-editing', 'Inline editing'), @@ -56,6 +57,27 @@ export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilde defaultValue: false, showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom, }); + + category = [t('canvas.category-tooltip', 'Tooltip')]; + + builder.addRadio({ + path: 'tooltip.mode', + name: t('canvas.tooltip-options.name-tooltip-mode', 'Tooltip mode'), + category, + defaultValue: TooltipDisplayMode.Single, + settings: { + options: [ + { + value: TooltipDisplayMode.Single, + label: t('canvas.tooltip-options.tooltip-mode-options.label-enabled', 'Enabled'), + }, + { + value: TooltipDisplayMode.None, + label: t('canvas.tooltip-options.tooltip-mode-options.label-disabled', 'Disabled'), + }, + ], + }, + }); }; export const plugin = new PanelPlugin(CanvasPanel) diff --git a/public/app/plugins/panel/canvas/panelcfg.cue b/public/app/plugins/panel/canvas/panelcfg.cue index 6da7cebacc4..f32837ea1e8 100644 --- a/public/app/plugins/panel/canvas/panelcfg.cue +++ b/public/app/plugins/panel/canvas/panelcfg.cue @@ -88,6 +88,9 @@ composableKinds: PanelCfg: { border?: LineConfig connections?: [...CanvasConnection] } @cuetsy(kind="interface") + CanvasTooltip: { + mode: ui.TooltipDisplayMode + } @cuetsy(kind="interface") Options: { // Enable inline editing @@ -108,6 +111,8 @@ composableKinds: PanelCfg: { // The list of canvas elements attached to the root element elements: [...CanvasElementOptions] } @cuetsy(kind="interface") + // Controls tooltip options + tooltip: CanvasTooltip } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/canvas/panelcfg.gen.ts b/public/app/plugins/panel/canvas/panelcfg.gen.ts index 9706c886f6d..be425f6c764 100644 --- a/public/app/plugins/panel/canvas/panelcfg.gen.ts +++ b/public/app/plugins/panel/canvas/panelcfg.gen.ts @@ -110,6 +110,10 @@ export const defaultCanvasElementOptions: Partial = { connections: [], }; +export interface CanvasTooltip { + mode: ui.TooltipDisplayMode; +} + export interface Options { /** * Enable infinite pan @@ -145,6 +149,10 @@ export interface Options { * Show all available element types */ showAdvancedTypes: boolean; + /** + * Controls tooltip options + */ + tooltip: CanvasTooltip; } export const defaultOptions: Partial = { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5e9f8d224bc..0a72605b908 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3738,6 +3738,7 @@ "category-selected-connection": "Selected connection", "category-selected-element": "Selected element ({{element}})", "category-size": "Size", + "category-tooltip": "Tooltip", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3982,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "Tooltip mode", + "tooltip-mode-options": { + "label-disabled": "Disabled", + "label-enabled": "Enabled" + } + }, "tree-navigation-editor": { "clear-selection": "Clear selection", "frame-selection": "Frame selection",