blob: 861437f2f95ca36473d99289fd47714c33b3b19f [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import "@xyflow/react/dist/style.css";
2import {
3 ReactFlow,
4 Background,
5 Controls,
6 Connection,
7 BackgroundVariant,
8 Edge,
9 useReactFlow,
10 Panel,
gioaf8db832025-05-13 14:43:05 +000011 useStoreApi,
giod0026612025-05-08 13:00:36 +000012} from "@xyflow/react";
gio359a6852025-05-14 03:38:24 +000013import { useStateStore, AppState, AppNode, useZoom } from "@/lib/state";
gio5f2f1002025-03-20 18:38:48 +040014import { useShallow } from "zustand/react/shallow";
giod0026612025-05-08 13:00:36 +000015import { useCallback, useEffect, useMemo } from "react";
gio5f2f1002025-03-20 18:38:48 +040016import { NodeGatewayHttps } from "@/components/node-gateway-https";
giod0026612025-05-08 13:00:36 +000017import { NodeApp } from "@/components/node-app";
18import { NodeVolume } from "./node-volume";
19import { NodePostgreSQL } from "./node-postgresql";
20import { NodeMongoDB } from "./node-mongodb";
21import { NodeGithub } from "./node-github";
22import { Actions } from "./actions";
23import { NodeGatewayTCP } from "./node-gateway-tcp";
24import { NodeNetwork } from "./node-network";
gio5f2f1002025-03-20 18:38:48 +040025
26const selector = (state: AppState) => ({
giod0026612025-05-08 13:00:36 +000027 nodes: state.nodes,
28 edges: state.edges,
29 onNodesChange: state.onNodesChange,
30 onEdgesChange: state.onEdgesChange,
31 onConnect: state.onConnect,
gio5f2f1002025-03-20 18:38:48 +040032});
33
34export function Canvas() {
giod0026612025-05-08 13:00:36 +000035 const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(useShallow(selector));
36 const store = useStateStore();
gioaf8db832025-05-13 14:43:05 +000037 const instance = useReactFlow();
38 const flow = useStoreApi();
gio359a6852025-05-14 03:38:24 +000039 const viewport = useZoom();
gioaf8db832025-05-13 14:43:05 +000040 useEffect(() => {
41 store.setViewport({
42 width: flow.getState().width,
43 height: flow.getState().height,
44 transformX: flow.getState().transform[0],
45 transformY: flow.getState().transform[1],
46 transformZoom: flow.getState().transform[2],
47 });
48 }, [store, flow]);
gio359a6852025-05-14 03:38:24 +000049 useEffect(() => {
50 instance.setViewport(viewport);
51 }, [viewport, instance]);
giod0026612025-05-08 13:00:36 +000052 const nodeTypes = useMemo(
53 () => ({
54 network: NodeNetwork,
55 app: NodeApp,
56 "gateway-https": NodeGatewayHttps,
57 "gateway-tcp": NodeGatewayTCP,
58 volume: NodeVolume,
59 postgresql: NodePostgreSQL,
60 mongodb: NodeMongoDB,
61 github: NodeGithub,
62 }),
63 [],
64 );
65 const isValidConnection = useCallback(
66 (c: Edge | Connection) => {
67 if (c.source === c.target) {
68 return false;
69 }
gioaf8db832025-05-13 14:43:05 +000070 const sn = instance.getNode(c.source)! as AppNode;
71 const tn = instance.getNode(c.target)! as AppNode;
giod0026612025-05-08 13:00:36 +000072 if (sn.type === "github") {
73 return c.targetHandle === "repository";
74 }
75 if (sn.type === "app") {
76 if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
77 return false;
78 }
79 }
80 if (tn.type === "gateway-https") {
81 if (c.targetHandle === "https" && tn.data.https !== undefined) {
82 return false;
83 }
84 }
85 if (sn.type === "volume") {
86 if (c.targetHandle !== "volume") {
87 return false;
88 }
89 return true;
90 }
91 if (tn.type === "network") {
92 if (c.sourceHandle !== "subdomain") {
93 return false;
94 }
95 if (sn.type !== "gateway-https" && sn.type !== "gateway-tcp") {
96 return false;
97 }
98 }
99 return true;
100 },
gioaf8db832025-05-13 14:43:05 +0000101 [instance],
giod0026612025-05-08 13:00:36 +0000102 );
giod0026612025-05-08 13:00:36 +0000103 return (
104 <div style={{ width: "100%", height: "100%" }}>
105 <ReactFlow
106 nodeTypes={nodeTypes}
107 nodes={nodes}
108 edges={edges}
109 onNodesChange={onNodesChange}
110 onEdgesChange={onEdgesChange}
111 onConnect={onConnect}
112 isValidConnection={isValidConnection}
113 fitView
114 proOptions={{ hideAttribution: true }}
115 >
gio818da4e2025-05-12 14:45:35 +0000116 <Controls showInteractive={false} />
117 <Background
118 variant={store.mode === "deploy" ? BackgroundVariant.Dots : BackgroundVariant.Lines}
119 gap={12}
120 size={1}
121 />
giod0026612025-05-08 13:00:36 +0000122 <Panel position="bottom-right">
123 <Actions />
124 </Panel>
125 </ReactFlow>
126 </div>
127 );
gio5f2f1002025-03-20 18:38:48 +0400128}
129
130export default Canvas;