Canvas: Add direction options for connections (#84620)

* Canvas: Add direction options for connections

* Fix radio control to handle default

* Fix broken options from merge

* Remove unused comment

* Change category and capitalize options for arrow

* Consolidate marker start and end for connections

* Fix linter issue
This commit is contained in:
Drew Slobodnjak 2024-03-21 11:38:17 -07:00 committed by GitHub
parent c4645c81da
commit c1065e6e68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 76 additions and 8 deletions

View File

@ -42,6 +42,13 @@ export enum ConnectionPath {
Straight = 'straight', Straight = 'straight',
} }
export enum ConnectionDirection {
Forward = 'forward',
Reverse = 'reverse',
Both = 'both',
None = 'none',
}
export interface CanvasConnection { export interface CanvasConnection {
source: ConnectionCoordinates; source: ConnectionCoordinates;
target: ConnectionCoordinates; target: ConnectionCoordinates;
@ -51,6 +58,7 @@ export interface CanvasConnection {
size?: ScaleDimensionConfig; size?: ScaleDimensionConfig;
lineStyle?: string; lineStyle?: string;
vertices?: ConnectionCoordinates[]; vertices?: ConnectionCoordinates[];
direction?: ConnectionDirection;
// See https://github.com/anseki/leader-line#options for more examples of more properties // See https://github.com/anseki/leader-line#options for more examples of more properties
} }

View File

@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { ConnectionDirection } from 'app/features/canvas';
import { Scene } from 'app/features/canvas/runtime/scene'; import { Scene } from 'app/features/canvas/runtime/scene';
import { ConnectionCoordinates } from '../../panelcfg.gen'; import { ConnectionCoordinates } from '../../panelcfg.gen';
@ -45,6 +46,7 @@ export const ConnectionSVG = ({
const EDITOR_HEAD_ID = useMemo(() => `editorHead-${headId}`, [headId]); const EDITOR_HEAD_ID = useMemo(() => `editorHead-${headId}`, [headId]);
const defaultArrowColor = config.theme2.colors.text.primary; const defaultArrowColor = config.theme2.colors.text.primary;
const defaultArrowSize = 2; const defaultArrowSize = 2;
const defaultArrowDirection = ConnectionDirection.Forward;
const maximumVertices = 10; const maximumVertices = 10;
const [selectedConnection, setSelectedConnection] = useState<ConnectionState | undefined>(undefined); const [selectedConnection, setSelectedConnection] = useState<ConnectionState | undefined>(undefined);
@ -134,14 +136,20 @@ export const ConnectionSVG = ({
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale); const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);
const midpoint = calculateMidpoint(x1, y1, x2, y2); const midpoint = calculateMidpoint(x1, y1, x2, y2);
const { strokeColor, strokeWidth, lineStyle } = getConnectionStyles(info, scene, defaultArrowSize); const { strokeColor, strokeWidth, arrowDirection, lineStyle } = getConnectionStyles(
info,
scene,
defaultArrowSize,
defaultArrowDirection
);
const isSelected = selectedConnection === v && scene.panel.context.instanceState.selectedConnection; const isSelected = selectedConnection === v && scene.panel.context.instanceState.selectedConnection;
const connectionCursorStyle = scene.isEditingEnabled ? 'grab' : ''; const connectionCursorStyle = scene.isEditingEnabled ? 'grab' : '';
const selectedStyles = { stroke: '#44aaff', strokeOpacity: 0.6, strokeWidth: strokeWidth + 5 }; const selectedStyles = { stroke: '#44aaff', strokeOpacity: 0.6, strokeWidth: strokeWidth + 5 };
const CONNECTION_HEAD_ID = `connectionHead-${headId + Math.random()}`; const CONNECTION_HEAD_ID_START = `connectionHeadStart-${headId + Math.random()}`;
const CONNECTION_HEAD_ID_END = `connectionHeadEnd-${headId + Math.random()}`;
// Create vertex path and populate array of add vertex controls // Create vertex path and populate array of add vertex controls
const addVertices: ConnectionCoordinates[] = []; const addVertices: ConnectionCoordinates[] = [];
@ -167,12 +175,33 @@ export const ConnectionSVG = ({
pathString += `L${x2} ${y2}`; pathString += `L${x2} ${y2}`;
} }
const markerStart =
arrowDirection === ConnectionDirection.Reverse || arrowDirection === ConnectionDirection.Both
? `url(#${CONNECTION_HEAD_ID_START})`
: undefined;
const markerEnd =
arrowDirection === ConnectionDirection.Forward || arrowDirection === ConnectionDirection.Both
? `url(#${CONNECTION_HEAD_ID_END})`
: undefined;
return ( return (
<svg className={styles.connection} key={idx}> <svg className={styles.connection} key={idx}>
<g onClick={() => selectConnection(v)}> <g onClick={() => selectConnection(v)}>
<defs> <defs>
<marker <marker
id={CONNECTION_HEAD_ID} id={CONNECTION_HEAD_ID_START}
markerWidth="10"
markerHeight="7"
refX="0"
refY="3.5"
orient="auto"
stroke={strokeColor}
>
<polygon points="10 0, 0 3.5, 10 7" fill={strokeColor} />
</marker>
<marker
id={CONNECTION_HEAD_ID_END}
markerWidth="10" markerWidth="10"
markerHeight="7" markerHeight="7"
refX="10" refX="10"
@ -201,7 +230,8 @@ export const ConnectionSVG = ({
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
strokeDasharray={lineStyle} strokeDasharray={lineStyle}
fill={'none'} fill={'none'}
markerEnd={`url(#${CONNECTION_HEAD_ID})`} markerEnd={markerEnd}
markerStart={markerStart}
/> />
{isSelected && ( {isSelected && (
<g> <g>
@ -262,8 +292,9 @@ export const ConnectionSVG = ({
stroke={strokeColor} stroke={strokeColor}
pointerEvents="auto" pointerEvents="auto"
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
markerEnd={markerEnd}
markerStart={markerStart}
strokeDasharray={lineStyle} strokeDasharray={lineStyle}
markerEnd={`url(#${CONNECTION_HEAD_ID})`}
x1={x1} x1={x1}
y1={y1} y1={y1}
x2={x2} x2={x2}

View File

@ -36,6 +36,7 @@ export function getConnectionEditor(opts: CanvasConnectionEditorOptions): Nested
const ctx = { ...context, options: opts.connection.info }; const ctx = { ...context, options: opts.connection.info };
optionBuilder.addColor(builder, ctx); optionBuilder.addColor(builder, ctx);
optionBuilder.addSize(builder, ctx); optionBuilder.addSize(builder, ctx);
optionBuilder.addDirection(builder, ctx);
optionBuilder.addLineStyle(builder, ctx); optionBuilder.addLineStyle(builder, ctx);
}, },
}; };

View File

@ -1,5 +1,7 @@
import { capitalize } from 'lodash';
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
import { CanvasConnection, CanvasElementOptions } from 'app/features/canvas'; import { CanvasConnection, CanvasElementOptions, ConnectionDirection } from 'app/features/canvas';
import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors'; import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
import { BackgroundSizeEditor } from 'app/features/dimensions/editors/BackgroundSizeEditor'; import { BackgroundSizeEditor } from 'app/features/dimensions/editors/BackgroundSizeEditor';
@ -12,6 +14,7 @@ interface OptionSuppliers {
addBorder: PanelOptionsSupplier<CanvasElementOptions>; addBorder: PanelOptionsSupplier<CanvasElementOptions>;
addColor: PanelOptionsSupplier<CanvasConnection>; addColor: PanelOptionsSupplier<CanvasConnection>;
addSize: PanelOptionsSupplier<CanvasConnection>; addSize: PanelOptionsSupplier<CanvasConnection>;
addDirection: PanelOptionsSupplier<CanvasConnection>;
addLineStyle: PanelOptionsSupplier<CanvasConnection>; addLineStyle: PanelOptionsSupplier<CanvasConnection>;
} }
@ -126,6 +129,24 @@ export const optionBuilder: OptionSuppliers = {
}); });
}, },
addDirection: (builder, context) => {
const category = ['Arrow Direction'];
builder.addRadio({
category,
path: 'direction',
name: 'Direction',
settings: {
options: [
{ value: undefined, label: capitalize(ConnectionDirection.Forward) },
{ value: ConnectionDirection.Reverse, label: capitalize(ConnectionDirection.Reverse) },
{ value: ConnectionDirection.Both, label: capitalize(ConnectionDirection.Both) },
{ value: ConnectionDirection.None, label: capitalize(ConnectionDirection.None) },
],
},
defaultValue: ConnectionDirection.Forward,
});
},
addLineStyle: (builder, context) => { addLineStyle: (builder, context) => {
const category = ['Line style']; const category = ['Line style'];
builder.addCustomEditor({ builder.addCustomEditor({

View File

@ -10,6 +10,7 @@ import {
canvasElementRegistry, canvasElementRegistry,
CanvasElementOptions, CanvasElementOptions,
CanvasConnection, CanvasConnection,
ConnectionDirection,
} from 'app/features/canvas'; } from 'app/features/canvas';
import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { ElementState } from 'app/features/canvas/runtime/element'; import { ElementState } from 'app/features/canvas/runtime/element';
@ -378,13 +379,19 @@ export const getRowIndex = (fieldName: string | undefined, scene: Scene) => {
return 0; return 0;
}; };
export const getConnectionStyles = (info: CanvasConnection, scene: Scene, defaultArrowSize: number) => { export const getConnectionStyles = (
info: CanvasConnection,
scene: Scene,
defaultArrowSize: number,
defaultArrowDirection: ConnectionDirection
) => {
const defaultArrowColor = config.theme2.colors.text.primary; const defaultArrowColor = config.theme2.colors.text.primary;
const lastRowIndex = getRowIndex(info.size?.field, scene); const lastRowIndex = getRowIndex(info.size?.field, scene);
const strokeColor = info.color ? scene.context.getColor(info.color).value() : defaultArrowColor; const strokeColor = info.color ? scene.context.getColor(info.color).value() : defaultArrowColor;
const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize; const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize;
const arrowDirection = info.direction ? info.direction : defaultArrowDirection;
const lineStyle = info.lineStyle === LineStyle.Dashed ? StrokeDasharray.Dashed : StrokeDasharray.Solid; const lineStyle = info.lineStyle === LineStyle.Dashed ? StrokeDasharray.Dashed : StrokeDasharray.Solid;
return { strokeColor, strokeWidth, lineStyle }; return { strokeColor, strokeWidth, arrowDirection, lineStyle };
}; };
export const getParentBoundingClientRect = (scene: Scene) => { export const getParentBoundingClientRect = (scene: Scene) => {