Canvas: Add option to disable the tooltip (#108196)

This commit is contained in:
Adela Almasan
2025-07-30 20:26:36 +00:00
committed by GitHub
parent 5b0a118f32
commit 9022357fb7
10 changed files with 106 additions and 59 deletions
@@ -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:
@@ -112,6 +112,10 @@ export const defaultCanvasElementOptions: Partial<CanvasElementOptions> = {
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<Options> = {
@@ -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,
});
+24 -19
View File
@@ -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<ReactZoomPanPinchContentRef> | 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 = (
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.setRef}>
@@ -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<Props, State> {
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<Props, State> {
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<Props, State> {
}
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<Props, State> {
};
tooltipCallback = (tooltip: CanvasTooltipPayload | undefined) => {
this.scene.tooltip = tooltip;
this.scene.tooltipPayload = tooltip;
this.forceUpdate();
};
@@ -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 && (
<Portal>
<VizTooltipContainer
className={cx(styles.tooltipWrapper, scene.tooltip.isOpen && styles.pinned)}
position={{ x: scene.tooltip.anchorPoint.x, y: scene.tooltip.anchorPoint.y }}
className={cx(styles.tooltipWrapper, scene.tooltipPayload.isOpen && styles.pinned)}
position={{ x: scene.tooltipPayload.anchorPoint.x, y: scene.tooltipPayload.anchorPoint.y }}
offset={{ x: 5, y: 0 }}
allowPointerEvents={scene.tooltip.isOpen}
allowPointerEvents={scene.tooltipPayload.isOpen}
>
<section ref={ref} {...overlayProps} {...dialogProps}>
{scene.tooltip.isOpen && <CloseButton style={{ zIndex: 1 }} onClick={onClose} />}
<VizTooltipHeader item={headerItem} isPinned={scene.tooltip.isOpen!} />
{element.data.text && <VizTooltipContent items={contentItems} isPinned={scene.tooltip.isOpen!} />}
{scene.tooltipPayload.isOpen && <CloseButton style={{ zIndex: 1 }} onClick={onClose} />}
<VizTooltipHeader item={headerItem} isPinned={scene.tooltipPayload.isOpen!} />
{element.data.text && <VizTooltipContent items={contentItems} isPinned={scene.tooltipPayload.isOpen!} />}
{(links.length > 0 || actions.length > 0) && <VizTooltipFooter dataLinks={links} actions={actions} />}
</section>
</VizTooltipContainer>
+23 -1
View File
@@ -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<Options>) => {
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<Options>(CanvasPanel)
@@ -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")
}
}]
@@ -110,6 +110,10 @@ export const defaultCanvasElementOptions: Partial<CanvasElementOptions> = {
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<Options> = {
+8
View File
@@ -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",