Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for creating a connector to a new node #3815

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion frontend/packages/topology/src/behavior/dnd-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type DragSource = {

export type DropTarget = {
type: TargetType;
dropHint(dndManager: DndManager): string | undefined;
drop(dndManager: DndManager): any;
hover(dndManager: DndManager): void;
canDrop(dndManager: DndManager): boolean;
Expand Down Expand Up @@ -52,6 +53,7 @@ export type Unregister = () => void;
export interface DndManager {
registerSource(source: DragSource): [string, Unregister];
registerTarget(target: DropTarget): [string, Unregister];
getDropHints(): string[] | undefined;
canDragSource(sourceId: string | undefined): boolean;
canDropOnTarget(targetId: string | undefined): boolean;
isDragging(): boolean;
Expand Down Expand Up @@ -137,10 +139,13 @@ export type DropTargetSpec<
Props extends {} = {}
> = {
accept: TargetType;
dropHint?:
| string
| ((item: DragObject, monitor: DropTargetMonitor, props: Props) => string | undefined);
hitTest?: (x: number, y: number, props: Props) => boolean;
drop?: (item: DragObject, monitor: DropTargetMonitor, props: Props) => DropResult | undefined;
hover?: (item: DragObject, monitor: DropTargetMonitor, props: Props) => void;
canDrop?: (item: DragObject, monitor: DropTargetMonitor, props: Props) => boolean;
canDrop?: boolean | ((item: DragObject, monitor: DropTargetMonitor, props: Props) => boolean);
collect?: (monitor: DropTargetMonitor, props: Props) => CollectedProps;
};

Expand All @@ -150,6 +155,7 @@ export interface HandlerManager {
}

export interface DragSourceMonitor extends HandlerManager {
getDropHints(): string[] | undefined;
canDrag(): boolean;
isCancelled(): boolean;
isDragging(): boolean;
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/topology/src/behavior/useDndDrag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export const useDndDrag = <
receiveHandlerId: (sourceId: string | undefined): void => {
idRef.current = sourceId;
},
getDropHints: (): string[] | undefined => {
return dndManager.getDropHints();
},
canDrag: (): boolean => {
return dndManager.canDragSource(idRef.current);
},
Expand Down
7 changes: 7 additions & 0 deletions frontend/packages/topology/src/behavior/useDndDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ export const useDndDrop = <
React.useEffect(() => {
const dropTarget: DropTarget = {
type: spec.accept,
dropHint: () => {
return typeof specRef.current.dropHint === 'string'
? specRef.current.dropHint
: typeof specRef.current.dropHint === 'function'
? specRef.current.dropHint(monitor.getItem(), monitor, propsRef.current)
: elementRef.current.getType();
},
hitTest: (x: number, y: number) => {
if (specRef.current.hitTest) {
return specRef.current.hitTest(x, y, propsRef.current);
Expand Down
18 changes: 17 additions & 1 deletion frontend/packages/topology/src/behavior/useDndManager.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { observable } from 'mobx';
import { computed, observable } from 'mobx';
import ControllerContext from '../utils/ControllerContext';
import {
DndManager,
Expand Down Expand Up @@ -45,6 +45,18 @@ export class DndManagerImpl implements DndManager {
@observable.shallow
private targets: { [key: string]: DropTarget } = {};

@computed
get dropHints(): string[] | undefined {
return this.state.targetIds
? (this.state.targetIds
.map((id) => {
const target = this.getTarget(id);
return target ? target.dropHint(this) : undefined;
})
.filter((x) => x) as string[])
: undefined;
}

registerSource(source: DragSource): [string, Unregister] {
const key = `S${getNextUniqueId()}`;
this.sources[key] = source;
Expand All @@ -67,6 +79,10 @@ export class DndManagerImpl implements DndManager {
];
}

getDropHints(): string[] | undefined {
return this.dropHints;
}

canDragSource(sourceId: string | undefined): boolean {
const source = this.getSource(sourceId);
if (!source || this.isDragging()) {
Expand Down
40 changes: 27 additions & 13 deletions frontend/packages/topology/src/behavior/withCreateConnector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import DefaultCreateConnector from '../components/DefaultCreateConnector';
import Point from '../geom/Point';
import Layer from '../components/layers/Layer';
import ContextMenu, { ContextMenuItem } from '../components/contextmenu/ContextMenu';
import { Node, isNode, AnchorEnd } from '../types';
import { Node, isNode, AnchorEnd, GraphElement, isGraph, Graph } from '../types';
import { DragSourceSpec, DragSourceMonitor, DragEvent } from './dnd-types';
import { useDndDrag } from './useDndDrag';

Expand All @@ -25,7 +25,7 @@ type CreateConnectorWidgetProps = {
onKeepAlive: (isAlive: boolean) => void;
onCreate: (
element: Node,
target: Node,
target: Node | Graph,
event: DragEvent,
choice?: ConnectorChoice,
) => ConnectorChoice[] | void | undefined | null;
Expand All @@ -35,16 +35,17 @@ type CreateConnectorWidgetProps = {
type CollectProps = {
event?: DragEvent;
dragging: boolean;
hints?: string[] | undefined;
};

type PromptData = {
element: Node;
target: Node;
target: Node | Graph;
event: DragEvent;
choices: ConnectorChoice[];
};

export const CREATE_CONNECTOR_DROP_TYPE = '#createConnector#"';
export const CREATE_CONNECTOR_DROP_TYPE = '#createConnector#';

const DEFAULT_HANDLE_ANGLE = 12 * (Math.PI / 180);
const DEFAULT_HANDLE_LENGTH = 32;
Expand All @@ -60,6 +61,7 @@ const CreateConnectorWidget: React.FC<CreateConnectorWidgetProps> = observer((pr
} = props;
const [prompt, setPrompt] = React.useState<PromptData | null>(null);
const [active, setActive] = React.useState(false);
const hintsRef = React.useRef<string[] | undefined>();

const spec = React.useMemo(() => {
const dragSourceSpec: DragSourceSpec<any, any, CollectProps> = {
Expand All @@ -73,12 +75,12 @@ const CreateConnectorWidget: React.FC<CreateConnectorWidgetProps> = observer((pr
p.element.raise();
},
end: (
dropResult: Node,
dropResult: GraphElement,
monitor: DragSourceMonitor,
dragProps: CreateConnectorWidgetProps,
) => {
const event = monitor.getDragEvent();
if (isNode(dropResult) && event) {
if ((isNode(dropResult) || isGraph(dropResult)) && event) {
const choices = dragProps.onCreate(dragProps.element, dropResult, event);
if (choices && choices.length) {
setPrompt({ element: dragProps.element, target: dropResult, event, choices });
Expand All @@ -91,17 +93,23 @@ const CreateConnectorWidget: React.FC<CreateConnectorWidgetProps> = observer((pr
collect: (monitor) => ({
dragging: !!monitor.getItem(),
event: monitor.isDragging() ? monitor.getDragEvent() : undefined,
hints: monitor.getDropHints(),
}),
};
return dragSourceSpec;
}, [setActive]);
const [{ dragging, event }, dragRef] = useDndDrag(spec, props);
const [{ dragging, event, hints }, dragRef] = useDndDrag(spec, props);

if (!active && dragging && !event) {
// another connector is dragging right now
return null;
}

if (dragging) {
// store the latest hints
hintsRef.current = hints;
}

const dragEvent = prompt ? prompt.event : event;

let startPoint: Point;
Expand Down Expand Up @@ -136,7 +144,7 @@ const CreateConnectorWidget: React.FC<CreateConnectorWidgetProps> = observer((pr
onMouseLeave={!active ? () => onKeepAlive(false) : undefined}
style={{ cursor: !dragging ? 'pointer' : undefined }}
>
{renderConnector(startPoint, endPoint)}
{renderConnector(startPoint, endPoint, hintsRef.current)}
{!active && (
<path
d={hullPath([[startPoint.x, startPoint.y], [endPoint.x, endPoint.y]], 7)}
Expand Down Expand Up @@ -179,11 +187,17 @@ export type WithCreateConnectorProps = {
onHideCreateConnector: () => void;
};

type ConnectorRenderer = (startPoint: Point, endPoint: Point) => React.ReactElement;

const defaultRenderConnector: ConnectorRenderer = (startPoint: Point, endPoint: Point) => (
<DefaultCreateConnector startPoint={startPoint} endPoint={endPoint} />
);
type ConnectorRenderer = <T extends any>(
startPoint: Point,
endPoint: Point,
hints: string[] | undefined,
) => React.ReactElement;

const defaultRenderConnector: ConnectorRenderer = (
startPoint: Point,
endPoint: Point,
hints?: string[] | undefined,
) => <DefaultCreateConnector startPoint={startPoint} endPoint={endPoint} hints={hints} />;

export const withCreateConnector = <P extends WithCreateConnectorProps & ElementProps>(
onCreate: React.ComponentProps<typeof CreateConnectorWidget>['onCreate'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
.topology-connector-arrow {
stroke: var(--pf-global--active-color--400);
fill: var(--pf-global--active-color--400);
}
}
&__line {
stroke: var(--pf-global--active-color--400);
stroke-width: 2px;
stroke-dasharray: 5px, 5px;
}

&__create > * {
stroke: var(--pf-global--active-color--400);
fill: #fff;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can use pf here --pf-chart-global--Fill--Color--white

stroke-width: 2px;
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import * as React from 'react';
import Point from '../geom/Point';
import './DefaultCreateConnector.scss';
import ConnectorArrow from './ConnectorArrow';

import './DefaultCreateConnector.scss';

type DefaultCreateConnectorProps = {
startPoint: Point;
endPoint: Point;
hints?: string[];
};

const DefaultCreateConnector: React.FC<DefaultCreateConnectorProps> = ({
startPoint,
endPoint,
hints,
}) => (
<g className="topology-default-create-connector">
<ConnectorArrow startPoint={startPoint} endPoint={endPoint} />
<line
className="topology-default-create-connector__line"
x1={startPoint.x}
y1={startPoint.y}
x2={endPoint.x}
y2={endPoint.y}
/>
{hints && hints.length === 1 && hints[0] === 'create' ? (
<g
transform={`translate(${endPoint.x},${endPoint.y})`}
className="topology-default-create-connector__create"
>
<circle cx={0} cy={0} r={6} />
<path d="M0,-3 V3 M-3,0 H3" />
</g>
) : (
<ConnectorArrow startPoint={startPoint} endPoint={endPoint} />
)}
</g>
);

Expand Down
28 changes: 25 additions & 3 deletions frontend/packages/topology/stories/4-Connector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
withDragNode,
WithDragNodeProps,
Layer,
Graph,
isGraph,
} from '../src';
import defaultComponentFactory from './components/defaultComponentFactory';
import DefaultEdge from './components/DefaultEdge';
Expand Down Expand Up @@ -199,13 +201,16 @@ export const createConnector = () => {
vis.registerComponentFactory(defaultComponentFactory);
vis.registerComponentFactory((kind) => {
if (kind === ModelKind.graph) {
return withPanZoom()(GraphComponent);
return withDndDrop({
accept: CREATE_CONNECTOR_DROP_TYPE,
dropHint: 'create',
})(withPanZoom()(GraphComponent));
}
if (kind === ModelKind.node) {
return withCreateConnector(
(
source: Node,
target: Node,
target: Node | Graph,
event: DragEvent,
choice: ColorChoice | undefined,
): any[] | null => {
Expand All @@ -216,6 +221,23 @@ export const createConnector = () => {
];
}

let targetId;
if (isGraph(target)) {
if (!model.nodes) {
model.nodes = [];
}
targetId = `n${vis.getGraph().getNodes().length + 1}`;
model.nodes.push({
id: targetId,
type: 'node',
x: event.x - 15,
y: event.y - 15,
height: 30,
width: 30,
});
} else {
targetId = target.getId();
}
const id = `e${vis.getGraph().getEdges().length + 1}`;
if (!model.edges) {
model.edges = [];
Expand All @@ -224,7 +246,7 @@ export const createConnector = () => {
id,
type: 'edge',
source: source.getId(),
target: target.getId(),
target: targetId,
data: {
color: choice.color,
},
Expand Down