From 589284778a37bf156bd9ff0c3d4f92c616eb4030 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 30 Jan 2023 21:59:51 +0000 Subject: [PATCH] [v9.4.x] Canvas: Connections positioning ux improvements (#62525) Canvas: Connections positioning ux improvements (#62516) (cherry picked from commit a92c081a3314b116ca38b816076310fe5bda5941) Co-authored-by: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> --- public/app/features/canvas/runtime/scene.tsx | 52 ++++++++++++++++--- .../app/plugins/panel/canvas/CanvasPanel.tsx | 12 +++++ .../plugins/panel/canvas/ConnectionSVG.tsx | 25 ++------- .../app/plugins/panel/canvas/Connections.tsx | 6 +++ public/app/plugins/panel/canvas/types.ts | 7 +++ public/app/plugins/panel/canvas/utils.ts | 30 ++++++++++- 6 files changed, 101 insertions(+), 31 deletions(-) diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index e171741273a..39034cdb554 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -78,6 +78,8 @@ export class Scene { tooltipCallback?: (tooltip: CanvasTooltipPayload | undefined) => void; tooltip?: CanvasTooltipPayload; + moveableActionCallback?: (moved: boolean) => void; + readonly editModeEnabled = new BehaviorSubject(false); subscription: Subscription; @@ -407,13 +409,29 @@ export class Scene { }) .on('drag', (event) => { const targetedElement = this.findElementByTarget(event.target); - targetedElement!.applyDrag(event); + if (targetedElement) { + targetedElement.applyDrag(event); + + if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) { + this.moveableActionCallback(true); + } + } }) .on('dragGroup', (e) => { - e.events.forEach((event) => { + let needsUpdate = false; + for (let event of e.events) { const targetedElement = this.findElementByTarget(event.target); - targetedElement!.applyDrag(event); - }); + if (targetedElement) { + targetedElement.applyDrag(event); + if (!needsUpdate) { + needsUpdate = this.connections.connectionsNeedUpdate(targetedElement); + } + } + } + + if (needsUpdate && this.moveableActionCallback) { + this.moveableActionCallback(true); + } }) .on('dragGroupEnd', (e) => { e.events.forEach((event) => { @@ -450,14 +468,32 @@ export class Scene { }) .on('resize', (event) => { const targetedElement = this.findElementByTarget(event.target); - targetedElement!.applyResize(event); + if (targetedElement) { + targetedElement.applyResize(event); + + if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) { + this.moveableActionCallback(true); + } + } this.moved.next(Date.now()); // TODO only on end }) .on('resizeGroup', (e) => { - e.events.forEach((event) => { + let needsUpdate = false; + for (let event of e.events) { const targetedElement = this.findElementByTarget(event.target); - targetedElement!.applyResize(event); - }); + if (targetedElement) { + targetedElement.applyResize(event); + + if (!needsUpdate) { + needsUpdate = this.connections.connectionsNeedUpdate(targetedElement); + } + } + } + + if (needsUpdate && this.moveableActionCallback) { + this.moveableActionCallback(true); + } + this.moved.next(Date.now()); // TODO only on end }) .on('resizeEnd', (event) => { diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index a78f2b2d821..16d86b6c6de 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -21,6 +21,7 @@ interface State { openInlineEdit: boolean; openSetBackground: boolean; contextMenuAnchorPoint: AnchorPoint; + moveableAction: boolean; } export interface InstanceState { @@ -56,6 +57,7 @@ export class CanvasPanel extends Component { openInlineEdit: false, openSetBackground: false, contextMenuAnchorPoint: { x: 0, y: 0 }, + moveableAction: false, }; // Only the initial options are ever used. @@ -72,6 +74,7 @@ export class CanvasPanel extends Component { this.scene.inlineEditingCallback = this.openInlineEdit; this.scene.setBackgroundCallback = this.openSetBackground; this.scene.tooltipCallback = this.tooltipCallback; + this.scene.moveableActionCallback = this.moveableActionCallback; this.subs.add( this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => { @@ -184,6 +187,10 @@ export class CanvasPanel extends Component { changed = true; } + if (this.state.moveableAction !== nextState.moveableAction) { + changed = true; + } + // After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing; const shouldShowAdvancedTypesSwitched = @@ -235,6 +242,11 @@ export class CanvasPanel extends Component { this.forceUpdate(); }; + moveableActionCallback = (updated: boolean) => { + this.setState({ moveableAction: updated }); + this.forceUpdate(); + }; + closeInlineEdit = () => { this.setState({ openInlineEdit: false }); isInlineEditOpen = false; diff --git a/public/app/plugins/panel/canvas/ConnectionSVG.tsx b/public/app/plugins/panel/canvas/ConnectionSVG.tsx index b0e99467898..31dc99e4755 100644 --- a/public/app/plugins/panel/canvas/ConnectionSVG.tsx +++ b/public/app/plugins/panel/canvas/ConnectionSVG.tsx @@ -7,18 +7,14 @@ import { CanvasConnection } from 'app/features/canvas/element'; import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; +import { getConnections } from './utils'; + type Props = { setSVGRef: (anchorElement: SVGSVGElement) => void; setLineRef: (anchorElement: SVGLineElement) => void; scene: Scene; }; -interface ConnectionInfo { - source: ElementState; - target: ElementState; - info: CanvasConnection; -} - let idCounter = 0; export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => { const styles = useStyles2(getStyles); @@ -89,22 +85,7 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => { // Flat list of all connections const findConnections = useCallback(() => { - const connections: ConnectionInfo[] = []; - for (let v of scene.byName.values()) { - if (v.options.connections) { - for (let c of v.options.connections) { - const target = c.targetName ? scene.byName.get(c.targetName) : v.parent; - if (target) { - connections.push({ - source: v, - target, - info: c, - }); - } - } - } - } - return connections; + return getConnections(scene.byName); }, [scene.byName]); // Figure out target and then target's relative coordinates drawing (if no target do parent) diff --git a/public/app/plugins/panel/canvas/Connections.tsx b/public/app/plugins/panel/canvas/Connections.tsx index 51d7be06523..3fe72811ff0 100644 --- a/public/app/plugins/panel/canvas/Connections.tsx +++ b/public/app/plugins/panel/canvas/Connections.tsx @@ -6,6 +6,7 @@ import { Scene } from 'app/features/canvas/runtime/scene'; import { CONNECTION_ANCHOR_ALT, ConnectionAnchors } from './ConnectionAnchors'; import { ConnectionSVG } from './ConnectionSVG'; +import { isConnectionSource, isConnectionTarget } from './utils'; export class Connections { scene: Scene; @@ -216,6 +217,11 @@ export class Connections { this.scene.selecto?.rootContainer?.addEventListener('mousemove', this.connectionListener); }; + // used for moveable actions + connectionsNeedUpdate = (element: ElementState): boolean => { + return isConnectionSource(element) || isConnectionTarget(element, this.scene.byName); + }; + render() { return ( <> diff --git a/public/app/plugins/panel/canvas/types.ts b/public/app/plugins/panel/canvas/types.ts index e5b2ee138d6..1e7b5958baf 100644 --- a/public/app/plugins/panel/canvas/types.ts +++ b/public/app/plugins/panel/canvas/types.ts @@ -1,3 +1,4 @@ +import { CanvasConnection } from '../../../features/canvas'; import { ElementState } from '../../../features/canvas/runtime/element'; export enum LayerActionID { @@ -31,3 +32,9 @@ export interface CanvasTooltipPayload { element: ElementState | undefined; isOpen?: boolean; } + +export interface ConnectionInfo { + source: ElementState; + target: ElementState; + info: CanvasConnection; +} diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts index 0b5e4b9c2c9..878359cabdb 100644 --- a/public/app/plugins/panel/canvas/utils.ts +++ b/public/app/plugins/panel/canvas/utils.ts @@ -16,7 +16,7 @@ import { FrameState } from '../../../features/canvas/runtime/frame'; import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene'; import { DimensionContext } from '../../../features/dimensions'; -import { AnchorPoint } from './types'; +import { AnchorPoint, ConnectionInfo } from './types'; export function doSelect(scene: Scene, element: ElementState | FrameState) { try { @@ -129,3 +129,31 @@ export function getDataLinks(ctx: DimensionContext, cfg: TextConfig, textData: s return links; } + +export function isConnectionSource(element: ElementState) { + return element.options.connections && element.options.connections.length > 0; +} +export function isConnectionTarget(element: ElementState, sceneByName: Map) { + const connections = getConnections(sceneByName); + return connections.some((connection) => connection.target === element); +} + +export function getConnections(sceneByName: Map) { + const connections: ConnectionInfo[] = []; + for (let v of sceneByName.values()) { + if (v.options.connections) { + for (let c of v.options.connections) { + const target = c.targetName ? sceneByName.get(c.targetName) : v.parent; + if (target) { + connections.push({ + source: v, + target, + info: c, + }); + } + } + } + } + + return connections; +}