blob: 2d78abd330ab49302ed46afbf0de9e5e750b3985 [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";
gio69148322025-06-19 23:16:12 +040013import { useStateStore, AppState, 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";
gio69148322025-06-19 23:16:12 +040025import { AppNode } from "config";
gio5f2f1002025-03-20 18:38:48 +040026
27const selector = (state: AppState) => ({
giod0026612025-05-08 13:00:36 +000028 nodes: state.nodes,
29 edges: state.edges,
30 onNodesChange: state.onNodesChange,
31 onEdgesChange: state.onEdgesChange,
32 onConnect: state.onConnect,
gio5f2f1002025-03-20 18:38:48 +040033});
34
gio3d0bf032025-06-05 06:57:26 +000035export function Canvas({ className }: { className?: string }) {
giod0026612025-05-08 13:00:36 +000036 const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(useShallow(selector));
37 const store = useStateStore();
gioaf8db832025-05-13 14:43:05 +000038 const instance = useReactFlow();
39 const flow = useStoreApi();
gio359a6852025-05-14 03:38:24 +000040 const viewport = useZoom();
gioaf8db832025-05-13 14:43:05 +000041 useEffect(() => {
42 store.setViewport({
43 width: flow.getState().width,
44 height: flow.getState().height,
45 transformX: flow.getState().transform[0],
46 transformY: flow.getState().transform[1],
47 transformZoom: flow.getState().transform[2],
48 });
49 }, [store, flow]);
gio359a6852025-05-14 03:38:24 +000050 useEffect(() => {
51 instance.setViewport(viewport);
52 }, [viewport, instance]);
giod0026612025-05-08 13:00:36 +000053 const nodeTypes = useMemo(
54 () => ({
55 network: NodeNetwork,
56 app: NodeApp,
57 "gateway-https": NodeGatewayHttps,
58 "gateway-tcp": NodeGatewayTCP,
59 volume: NodeVolume,
60 postgresql: NodePostgreSQL,
61 mongodb: NodeMongoDB,
62 github: NodeGithub,
63 }),
64 [],
65 );
66 const isValidConnection = useCallback(
67 (c: Edge | Connection) => {
68 if (c.source === c.target) {
69 return false;
70 }
gioaf8db832025-05-13 14:43:05 +000071 const sn = instance.getNode(c.source)! as AppNode;
72 const tn = instance.getNode(c.target)! as AppNode;
gio69148322025-06-19 23:16:12 +040073
giod0026612025-05-08 13:00:36 +000074 if (sn.type === "github") {
75 return c.targetHandle === "repository";
76 }
77 if (sn.type === "app") {
78 if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
79 return false;
80 }
81 }
82 if (tn.type === "gateway-https") {
83 if (c.targetHandle === "https" && tn.data.https !== undefined) {
84 return false;
85 }
86 }
87 if (sn.type === "volume") {
88 if (c.targetHandle !== "volume") {
89 return false;
90 }
91 return true;
92 }
93 if (tn.type === "network") {
94 if (c.sourceHandle !== "subdomain") {
95 return false;
96 }
97 if (sn.type !== "gateway-https" && sn.type !== "gateway-tcp") {
98 return false;
99 }
100 }
101 return true;
102 },
gioaf8db832025-05-13 14:43:05 +0000103 [instance],
giod0026612025-05-08 13:00:36 +0000104 );
giod0026612025-05-08 13:00:36 +0000105 return (
gio3d0bf032025-06-05 06:57:26 +0000106 <div style={{ width: "100%", height: "100%" }} className={className}>
giod0026612025-05-08 13:00:36 +0000107 <ReactFlow
108 nodeTypes={nodeTypes}
109 nodes={nodes}
110 edges={edges}
111 onNodesChange={onNodesChange}
112 onEdgesChange={onEdgesChange}
113 onConnect={onConnect}
114 isValidConnection={isValidConnection}
115 fitView
116 proOptions={{ hideAttribution: true }}
117 >
gio818da4e2025-05-12 14:45:35 +0000118 <Controls showInteractive={false} />
119 <Background
120 variant={store.mode === "deploy" ? BackgroundVariant.Dots : BackgroundVariant.Lines}
121 gap={12}
122 size={1}
123 />
gio3d0bf032025-06-05 06:57:26 +0000124 <Panel position="top-right">
giod0026612025-05-08 13:00:36 +0000125 <Actions />
126 </Panel>
127 </ReactFlow>
128 </div>
129 );
gio5f2f1002025-03-20 18:38:48 +0400130}
131
132export default Canvas;