Skip to content
This repository was archived by the owner on Jun 13, 2024. It is now read-only.

Commit e11d7be

Browse files
committed
refactor!: begun separation of core logic and reactflow
1 parent 87dad7e commit e11d7be

17 files changed

Lines changed: 673 additions & 1033 deletions

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
@Jaryt
12
@KyleTryon
2-
@CircleCI-Public/cpeng
3+
@CircleCI-Public/cpeng

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@circleci/circleci-config-parser": "^0.10.0-alpha.3",
1515
"@circleci/circleci-config-sdk": "^0.10.1",
1616
"@monaco-editor/react": "^4.4.5",
17+
"@vitejs/plugin-react": "^2.2.0",
1718
"algoliasearch": "^4.13.1",
1819
"easy-peasy": "^5.0.3",
1920
"formik": "^2.2.9",
@@ -24,7 +25,7 @@
2425
"react-dom": "^18.2.0",
2526
"react-instantsearch-hooks-web": "^6.29.0",
2627
"react-redux": "^7.2.6",
27-
"reactflow": "11.0.0",
28+
"reactflow": "^11.2.0",
2829
"start-server-and-test": "^1.14.0",
2930
"uuid": "^8.3.2"
3031
},

src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import Toast from './components/atoms/Toast';
55
import ToolTip from './components/atoms/Tooltip';
66
import ConfirmationModal from './components/containers/ConfirmationModal';
77
import KBarList from './components/containers/KBarList';
8-
import { FlowProvided } from './components/flow/Flow';
98
import EditorPane from './components/panes/EditorPane';
109
import NavigationPane from './components/panes/NavigationPane';
1110
import WorkflowsPane from './components/panes/WorkflowsPane';

src/components/atoms/Button.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ButtonHTMLAttributes } from 'react';
22

3-
const styles = {
3+
const styles: Record<string, { default: string, active: string, selected?: string }> = {
44
dangerous: {
55
default: 'bg-circle-red-dangerous text-white',
6-
active: 'hover:bg-circle-red-dangerous-dark ',
6+
active: 'hover:bg-circle-red-dangerous-dark',
77
},
88
secondary: {
99
default: 'bg-circle-gray-250',
1010
active: 'hover:bg-circle-gray-300',
11+
selected: 'bg-circle-gray-400'
1112
},
1213
flat: {
1314
default: 'text-circle-gray-400',
@@ -25,10 +26,12 @@ export const Button = ({
2526
variant,
2627
className,
2728
margin,
29+
selected,
2830
...props
2931
}: ButtonHTMLAttributes<HTMLButtonElement> & {
3032
variant: ButtonVariant;
3133
margin?: string;
34+
selected?: boolean;
3235
}) => {
3336
return (
3437
<button
@@ -39,7 +42,8 @@ export const Button = ({
3942
styles[variant].default
4043
}
4144
${className}
42-
${props.disabled ? 'opacity-50 cursor-default' : styles[variant].active}`}
45+
${props.disabled ? 'opacity-50 cursor-default' : styles[variant].active}
46+
${selected && styles[variant].selected}`}
4347
>
4448
{props.children}
4549
</button>

src/components/containers/FlowTools.tsx

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/components/containers/HeaderMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Button } from '../atoms/Button';
22
import { ExternalLinks } from './ExternalLinks';
3-
import { FlowTools } from './FlowTools';
3+
import { FlowTools } from '../flow/FlowTools';
44
import PreviewToolbox from './PreviewToolbox';
55

66
export default function HeaderMenu() {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ConnectionLineComponentProps, getBezierPath, MarkerType } from 'reactflow';
2+
import { useStoreState } from '../../state/Hooks';
3+
4+
function ConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle, fromNode, fromHandle }: ConnectionLineComponentProps) {
5+
const stepDist = 20;
6+
const realStartX = fromX + (fromNode?.width || 2) / 2;
7+
const isSource = fromHandle?.position == 'right';
8+
useStoreState((state) => state.mode);
9+
const valid = isSource ? toX >= realStartX + stepDist : toX <= realStartX + stepDist;
10+
const color = '#76CDFF';
11+
12+
const edgePath = valid ? getSteppedPath(realStartX, fromY, toX, toY, stepDist) : `M${realStartX},${fromY} ${realStartX + stepDist},${fromY}`;
13+
14+
return (
15+
<g>
16+
<defs>
17+
<marker
18+
id="triangle"
19+
viewBox="0 0 4 4"
20+
refX="1"
21+
refY="2"
22+
markerUnits="strokeWidth"
23+
markerWidth="4"
24+
markerHeight="4"
25+
orient="auto">
26+
<path d="M 0 0 L 4 2 L 0 4 z" fill={color} strokeWidth={1.5}/>
27+
</marker>
28+
</defs>
29+
<path style={connectionLineStyle} className="animated" fill="none" strokeWidth={1.5} stroke={color} d={edgePath} markerEnd="url(#triangle)" />
30+
{valid && <circle cx={toX} cy={toY} fill="white" r={3} stroke={color} strokeWidth={1.5} /> }
31+
</g>
32+
);
33+
}
34+
35+
export function getSteppedPath(sourceX: number, sourceY: number, targetX: number, targetY: number, dist: number ) {
36+
return `M${sourceX},${sourceY} ${sourceX + dist},${sourceY} ${
37+
sourceX + dist
38+
},${sourceY} ${targetX - dist},${targetY} ${targetX - dist},${targetY} ${
39+
targetX
40+
},${targetY}`;
41+
}
42+
43+
export default ConnectionLine;

src/components/flow/Flow.tsx

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,82 @@
1-
import ReactFlow, { Background, BackgroundVariant, ControlButton, Controls, MiniMap, ReactFlowProvider, useReactFlow } from "reactflow";
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import ReactFlow, { Background, useOnViewportChange, BackgroundVariant, ControlButton, Controls, MiniMap, Node, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStoreApi, useViewport, Viewport, XYPosition, applyNodeChanges, applyEdgeChanges, addEdge, Edge, Connection, ConnectionMode } from "reactflow";
3+
import { FlowMode } from "../../state/FlowStore";
24
import { useStoreState } from "../../state/Hooks";
5+
import ConnectionLine from "./ConnectionLine";
6+
import JobNode from "./JobNode";
37

48
export type FlowProps = { className?: string }
59

10+
11+
const nodeTypes = {
12+
job: JobNode,
13+
};
14+
15+
const initialNodes = [
16+
{
17+
id: '1',
18+
type: 'job',
19+
data: { label: 'Node A' },
20+
position: { x: 250, y: 0 },
21+
},
22+
{
23+
id: '2',
24+
type: 'job',
25+
data: { label: 'Node B' },
26+
position: { x: 100, y: 200 },
27+
},
28+
{
29+
id: '3',
30+
type: 'job',
31+
data: { label: 'Node C' },
32+
position: { x: 350, y: 200 },
33+
},
34+
];
35+
36+
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'updatable edge' }];
37+
const MIN_DISTANCE = 1000;
38+
639
const Flow = (props: FlowProps) => {
7-
// const reactFlowInstance = useReactFlow();
40+
const store = useStoreApi();
841
const dragging = useStoreState((state) => state.dragging);
42+
const viewport = useViewport();
43+
const flowRef = useRef<HTMLDivElement>(null);
44+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
45+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
46+
const mode = useStoreState((state) => state.mode);
47+
const onConnect = useCallback((params: Edge | Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
48+
949

1050
return <ReactFlow
1151
minZoom={-Infinity}
1252
fitView
13-
// nodes={nodes}
14-
// edges={edges as Edge[]}
15-
// onNodeClick={handleNodeClick}
16-
className={props.className}
53+
nodes={nodes}
54+
edges={edges}
55+
ref={flowRef}
56+
onNodesChange={onNodesChange}
57+
onEdgesChange={onEdgesChange}
58+
onConnect={onConnect}
59+
nodesDraggable={mode == FlowMode.MOVE}
60+
elementsSelectable={mode == FlowMode.SELECT}
61+
connectionLineComponent={ConnectionLine}
62+
snapToGrid
63+
nodeTypes={nodeTypes}
64+
className={props.className}
1765
onDragOver={(e) => {
1866
if (dragging?.dataType?.dragTarget === 'workflow') {
1967
e.preventDefault();
2068
}
2169
}}
70+
onMouseMove={(event) => {
71+
// getClosestNode({ x: event.clientX, y: event.clientY });
72+
}}
2273
onInit={(reactFlowInstance) => console.log('flow loaded:', reactFlowInstance)}
2374
>
2475
<Background
2576
gap={15}
2677
color="#c7c7c7"
2778
className="bg-circle-gray-200"
28-
size={2}
79+
size={2}
2980
/>
3081
<Controls>
3182
</Controls>
@@ -38,4 +89,55 @@ const GraphWrapper = (props: FlowProps) => {
3889
</ReactFlowProvider>
3990
}
4091

41-
export { GraphWrapper, Flow };
92+
export { GraphWrapper, Flow };
93+
// const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []);
94+
95+
// const onNodesChange = useCallback(
96+
// (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
97+
// [setNodes]
98+
// );
99+
// const onEdgesChange = useCallback(
100+
// (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
101+
// [setEdges]
102+
// );
103+
// const onConnect = useCallback(
104+
// (connection) => setEdges((eds) => addEdge(connection, eds)),
105+
// [setEdges]
106+
// );
107+
108+
// const getClosestNode = useCallback((pos: XYPosition) => {
109+
// const { nodeInternals } = store.getState();
110+
// const storeNodes = Array.from(nodeInternals.values());
111+
112+
// const closestNode = storeNodes.reduce<{ distance: number, node?: Node<any>}>(
113+
// (res, n) => {
114+
// const boundingRect = flowRef.current?.getBoundingClientRect() || { x: 0, y: 0};
115+
// const mouseX = Math.abs((pos.x - boundingRect.x) * viewport.zoom + viewport.x);
116+
// const mouseY = Math.abs((pos.y - boundingRect.y) * viewport.zoom + viewport.y)
117+
// const dx = Math.abs(n.position.x) - mouseX;
118+
// const dy = Math.abs(n.position.y) - mouseY;
119+
// const d = Math.sqrt(dx * dx + dy * dy);
120+
121+
// // console.log(pos.y - boundingRect.y, mouseY, viewport.y)
122+
123+
// if (d < res.distance && d < MIN_DISTANCE) {
124+
// res.distance = d;
125+
// res.node = n;
126+
// }
127+
128+
// return res;
129+
// },
130+
// {
131+
// distance: Number.MAX_VALUE,
132+
// node: undefined,
133+
// }
134+
// );
135+
136+
// if (!closestNode.node) {
137+
// return null;
138+
// }
139+
140+
// // console.log(closestNode.node)
141+
142+
// return closestNode.node;
143+
// }, [viewport]);

src/components/flow/FlowTools.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useCallback, useMemo } from "react";
2+
import { FlowMode } from "../../state/FlowStore";
3+
import { useStoreState, useStoreActions } from "../../state/Hooks"
4+
import { Button } from "../atoms/Button"
5+
import PreviewToolbox from "../containers/PreviewToolbox"
6+
7+
const buttons = [
8+
{
9+
text: 'Select',
10+
mode: FlowMode.SELECT,
11+
},
12+
{
13+
text: 'Connect',
14+
mode: FlowMode.CONNECT,
15+
},
16+
{
17+
text: 'Move',
18+
mode: FlowMode.MOVE,
19+
}
20+
]
21+
22+
const FlowTools = () => {
23+
const mode = useStoreState((state) => state.mode);
24+
const setMode = useStoreActions((actions) => actions.setMode)
25+
26+
const updateMode = useCallback((mode: FlowMode) => {
27+
setMode(mode);
28+
}, [mode, setMode])
29+
30+
const Buttons = useMemo(() => {
31+
return buttons.map((flowMapping, i) => {
32+
const className = i == 0 ? 'mr-0 rounded-r-none' : i == buttons.length - 1 ? 'ml-0 rounded-l-none' : 'mx-0 rounded-none';
33+
return (<Button key={flowMapping.mode} selected={mode == flowMapping.mode} variant="secondary" className={className} onClick={() => updateMode(flowMapping.mode)}>
34+
{flowMapping.text}
35+
</Button>)
36+
})
37+
}, [mode, updateMode ])
38+
39+
return (<div className="mr-2 px-4 flex flex-row border-r border-r-circle-gray-300">
40+
{Buttons}
41+
<PreviewToolbox />
42+
</div>)
43+
}
44+
45+
export { FlowTools }

src/components/flow/JobNode.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useMemo } from 'react';
2+
import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow';
3+
import JobIcon from '../../icons/components/JobIcon';
4+
import { FlowMode } from '../../state/FlowStore';
5+
import { useStoreState } from '../../state/Hooks';
6+
7+
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId;
8+
9+
export default function JobNode({ id, selected }: NodeProps) {
10+
const connectionNodeId = useStore(connectionNodeIdSelector);
11+
const mode = useStoreState((state) => state.mode);
12+
13+
const isTarget = connectionNodeId && connectionNodeId !== id;
14+
15+
const label = isTarget ? 'Drop here' : 'Drag to connect';
16+
const nodeBase = "p-2 pr-3 text-sm bg-white node flex flex-row text-black rounded-md border cursor-pointer"
17+
const outline = selected ? "border-circle-blue" : " border-circle-gray-300"
18+
19+
const Handles = useMemo(() => {
20+
const connectMode = mode == FlowMode.CONNECT;
21+
const handleBase = `${ connectMode ? '!w-full !h-full' : '!w-0 h-0'} !rounded opacity-0`
22+
const notConnecting = connectionNodeId == null
23+
24+
return (
25+
<>
26+
<Handle
27+
className={`${handleBase} ${isTarget ? 'z-0' : !connectMode ? '-z-10' : 'z-10'}`}
28+
style={{transform: notConnecting ? 'translate(-5px, -50%)' : ''}}
29+
position={Position.Right}
30+
type="source"
31+
/>
32+
<Handle
33+
className={`${handleBase} ${isTarget ? 'z-10' : !connectMode ? '-z-10' : 'z-0'}`}
34+
style={{transform: 'translate(4px, -50%)'}}
35+
position={Position.Left}
36+
type="target"
37+
/>
38+
</>
39+
)
40+
41+
}, [mode, connectionNodeId])
42+
43+
return (
44+
<div className={`${nodeBase} ${outline} z-10`}>
45+
<JobIcon className="w-4 mr-2"/>
46+
{label}
47+
{Handles}
48+
</div>
49+
);
50+
}

0 commit comments

Comments
 (0)