Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/components/canvas/canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@xyflow/react/dist/style.css';
import styled from '@emotion/styled';
import { Background, ProOptions, ReactFlow, ReactFlowProps, useEdgesState, useNodesState } from '@xyflow/react';
import { useEffect } from 'react';

import { MiniMap } from '@/components/controls/mini-map';
import { Controls } from '@/components/controls/controls';
Expand All @@ -11,6 +12,7 @@ import { InternalEdge, InternalNode } from '@/types/internal';
import { FloatingEdge } from '@/components/edge/floating-edge';
import { SelfReferencingEdge } from '@/components/edge/self-referencing-edge';
import { MarkerList } from '@/components/markers/marker-list';
import { ConnectionLine } from '@/components/line/connection-line';

const MAX_ZOOM = 3;
const MIN_ZOOM = 0.1;
Expand All @@ -34,13 +36,23 @@ const edgeTypes = {
selfReferencingEdge: SelfReferencingEdge,
};

type Props = Pick<ReactFlowProps, 'title'> & { nodes: ExternalNode[]; edges: Edge[] };
type Props = Pick<ReactFlowProps, 'title' | 'onConnect'> & { nodes: ExternalNode[]; edges: Edge[] };

export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges }: Props) => {
export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges, onConnect, ...rest }: Props) => {
const { initialNodes, initialEdges } = useCanvas(externalNodes, externalEdges);

const [nodes, , onNodesChange] = useNodesState<InternalNode>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<InternalEdge>(initialEdges);
const [nodes, setNodes, onNodesChange] = useNodesState<InternalNode>(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState<InternalEdge>(initialEdges);

useEffect(() => {
setNodes(initialNodes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialNodes]);

useEffect(() => {
setEdges(initialEdges);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialEdges]);

return (
<ReactFlowWrapper>
Expand All @@ -54,8 +66,11 @@ export const Canvas = ({ title, nodes: externalNodes, edges: externalEdges }: Pr
nodes={nodes}
onlyRenderVisibleElements={true}
edges={edges}
connectionLineComponent={ConnectionLine}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
{...rest}
>
<MarkerList />
<Background />
Expand Down
6 changes: 5 additions & 1 deletion src/components/canvas/use-canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useCanvas } from './use-canvas';

describe('use-canvas', () => {
it('Should get initial nodes', () => {
const { result } = renderHook(() => useCanvas([ORDERS_NODE, EMPLOYEES_NODE], []));
const { result } = renderHook(() => useCanvas([{ ...ORDERS_NODE, disabled: true }, EMPLOYEES_NODE], []));
expect(result.current.initialNodes).toEqual([
{
id: 'orders',
Expand All @@ -15,12 +15,14 @@ describe('use-canvas', () => {
x: 100,
y: 100,
},
draggable: false,
measured: {
height: 36,
width: 244,
},
data: {
borderVariant: undefined,
disabled: true,
fields: [
{ name: 'ORDER_ID', type: 'varchar', glyphs: ['key'] },
{ name: 'SUPPLIER_ID', type: 'varchar', glyphs: ['link'] },
Expand All @@ -39,7 +41,9 @@ describe('use-canvas', () => {
height: 72,
width: 244,
},
draggable: true,
data: {
disabled: undefined,
borderVariant: undefined,
fields: [
{ name: 'employeeId', type: 'objectId', glyphs: ['key'] },
Expand Down
4 changes: 3 additions & 1 deletion src/components/canvas/use-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ export const useCanvas = (externalNodes: ExternalNode[], externalEdges: Edge[])
const initialNodes: InternalNode[] = useMemo(
() =>
externalNodes.map(node => {
const { title, fields, borderVariant, ...rest } = node;
const { title, fields, borderVariant, disabled, ...rest } = node;
return {
...rest,
draggable: !disabled,
data: {
title,
disabled,
fields,
borderVariant,
},
Expand Down
42 changes: 42 additions & 0 deletions src/components/diagram.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Connection } from '@xyflow/react';

import { Diagram } from '@/components/diagram';
import { EMPLOYEES_NODE, ORDERS_NODE } from '@/mocks/datasets/nodes';
import { EMPLOYEES_TO_EMPLOYEES_EDGE, ORDERS_TO_EMPLOYEES_EDGE } from '@/mocks/datasets/edges';
import { Edge } from '@/types';

const diagram: Meta<typeof Diagram> = {
title: 'Diagram',
Expand All @@ -18,3 +21,42 @@ const diagram: Meta<typeof Diagram> = {
export default diagram;
type Story = StoryObj<typeof Diagram>;
export const BasicDiagram: Story = {};
export const DiagramWithConnectableNodes: Story = {
decorators: [
(Story, context) => {
const [edges, setEdges] = useState<Edge[]>(context.args.edges);
const onConnect = (connection: Connection) => {
setEdges([
...edges,
{
...ORDERS_TO_EMPLOYEES_EDGE,
source: connection.source,
target: connection.target,
animated: true,
selected: true,
},
]);
};

const onPaneClick = () => {
setEdges(edges.filter(edge => edge.id !== ORDERS_TO_EMPLOYEES_EDGE.id));
};

return Story({
...context,
args: {
...context.args,
edges,
onPaneClick,
onConnect,
},
});
},
],
args: {
title: 'MongoDB Diagram',
isDarkMode: true,
edges: [],
nodes: [ORDERS_NODE, EMPLOYEES_NODE],
},
};
14 changes: 14 additions & 0 deletions src/components/line/connection-line.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render, screen } from '@/mocks/testing-utils';
import { ConnectionLine } from '@/components/line/connection-line';

describe('connection-line', () => {
const renderComponent = () => {
return render(<ConnectionLine fromX={100} fromY={100} toX={100} toY={100} />);
};

it('Should render line', () => {
renderComponent();
const path = screen.getByTestId('connection-line');
expect(path).toHaveAttribute('d', 'M 100,100L 100,100');
});
});
28 changes: 28 additions & 0 deletions src/components/line/connection-line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { palette } from '@leafygreen-ui/palette';
import { ConnectionLineComponentProps, getStraightPath } from '@xyflow/react';

type Props = Pick<ConnectionLineComponentProps, 'fromX' | 'fromY' | 'toX' | 'toY'>;

export const ConnectionLine = ({ fromX, fromY, toX, toY }: Props) => {
const [edgePath] = getStraightPath({
sourceX: fromX,
sourceY: fromY,
targetX: toX,
targetY: toY,
});

return (
<g>
<circle cx={fromX} cy={fromY} fill={palette.blue.base} r={4} />
<path
data-testid={'connection-line'}
style={{ animation: 'dashdraw 0.5s linear infinite' }}
d={edgePath}
fill="none"
stroke={palette.blue.base}
strokeDasharray={5}
strokeWidth={2}
/>
</g>
);
};
23 changes: 19 additions & 4 deletions src/components/node/node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,25 @@ const NodeHeaderTitle = styled.div`
`;

const NodeHandle = styled(Handle)`
visibility: hidden;
width: 100%;
height: 100%;
background: blue;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
transform: none;
border: none;
opacity: 0;
z-index: 1;
`;

export const Node = ({ type, selected, data: { title, fields, borderVariant, disabled } }: NodeProps<InternalNode>) => {
export const Node = ({
type,
selected,
isConnectable,
data: { title, fields, borderVariant, disabled },
}: NodeProps<InternalNode>) => {
const theme = useTheme();
const { zoom } = useViewport();

Expand Down Expand Up @@ -126,8 +141,8 @@ export const Node = ({ type, selected, data: { title, fields, borderVariant, dis
return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<NodeBorder variant={selected ? 'selected' : borderVariant}>
<NodeHandle id="source" position={Position.Right} type="source" />
<NodeHandle id="source" position={Position.Left} type="target" />
<NodeHandle id="source" isConnectable={isConnectable} position={Position.Right} type="source" />
<NodeHandle id="source" isConnectable={isConnectable} position={Position.Left} type="target" />
<NodeWrapper accent={getAccent()} color={getNodeColor()}>
<NodeHeader background={getHeaderBackground()}>
{!isContextualZoom && (
Expand Down
2 changes: 1 addition & 1 deletion src/types/component-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ReactFlowProps } from '@xyflow/react';
import { Edge } from '@/types/edge';
import { Node } from '@/types/node';

type BaseProps = Pick<ReactFlowProps, 'title'>;
type BaseProps = Pick<ReactFlowProps, 'title' | 'onConnect' | 'onPaneClick'>;

export interface DiagramProps extends BaseProps {
isDarkMode?: boolean;
Expand Down
5 changes: 4 additions & 1 deletion src/types/edge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Edge as ReactFlowEdge } from '@xyflow/react';

export type BaseEdgeProps = Pick<ReactFlowEdge, 'id' | 'type' | 'source' | 'target' | 'hidden' | 'selected'>;
export type BaseEdgeProps = Pick<
ReactFlowEdge,
'id' | 'type' | 'source' | 'target' | 'hidden' | 'selected' | 'animated'
>;

export type Marker = 'one' | 'many' | 'oneOrMany';

Expand Down
2 changes: 1 addition & 1 deletion src/types/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NodeData } from '@/types/internal';

export type BaseNodeProps = Pick<
ReactFlowNode<{}, NodeType>,
'id' | 'type' | 'position' | 'hidden' | 'draggable' | 'selected' | 'style' | 'className' | 'measured'
'id' | 'type' | 'position' | 'hidden' | 'draggable' | 'connectable' | 'selected' | 'style' | 'className' | 'measured'
>;
export type NodeType = 'table' | 'collection';
export type NodeBorderVariant = 'subtle' | 'preview' | 'selected' | 'none';
Expand Down