blob: 122de3f149bf3730ec7f6cc5d849ad7621a11139 [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,
11} from "@xyflow/react";
12import { useStateStore, AppState, AppNode, useEnv } from "@/lib/state";
gio5f2f1002025-03-20 18:38:48 +040013import { useShallow } from "zustand/react/shallow";
giod0026612025-05-08 13:00:36 +000014import { useCallback, useEffect, useMemo } from "react";
gio5f2f1002025-03-20 18:38:48 +040015import { NodeGatewayHttps } from "@/components/node-gateway-https";
giod0026612025-05-08 13:00:36 +000016import { NodeApp } from "@/components/node-app";
17import { NodeVolume } from "./node-volume";
18import { NodePostgreSQL } from "./node-postgresql";
19import { NodeMongoDB } from "./node-mongodb";
20import { NodeGithub } from "./node-github";
21import { Actions } from "./actions";
22import { NodeGatewayTCP } from "./node-gateway-tcp";
23import { NodeNetwork } from "./node-network";
gio5f2f1002025-03-20 18:38:48 +040024
25const selector = (state: AppState) => ({
giod0026612025-05-08 13:00:36 +000026 nodes: state.nodes,
27 edges: state.edges,
28 onNodesChange: state.onNodesChange,
29 onEdgesChange: state.onEdgesChange,
30 onConnect: state.onConnect,
gio5f2f1002025-03-20 18:38:48 +040031});
32
33export function Canvas() {
giod0026612025-05-08 13:00:36 +000034 const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(useShallow(selector));
35 const store = useStateStore();
36 const flow = useReactFlow();
37 const nodeTypes = useMemo(
38 () => ({
39 network: NodeNetwork,
40 app: NodeApp,
41 "gateway-https": NodeGatewayHttps,
42 "gateway-tcp": NodeGatewayTCP,
43 volume: NodeVolume,
44 postgresql: NodePostgreSQL,
45 mongodb: NodeMongoDB,
46 github: NodeGithub,
47 }),
48 [],
49 );
50 const isValidConnection = useCallback(
51 (c: Edge | Connection) => {
52 if (c.source === c.target) {
53 return false;
54 }
55 const sn = flow.getNode(c.source)! as AppNode;
56 const tn = flow.getNode(c.target)! as AppNode;
57 if (sn.type === "github") {
58 return c.targetHandle === "repository";
59 }
60 if (sn.type === "app") {
61 if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
62 return false;
63 }
64 }
65 if (tn.type === "gateway-https") {
66 if (c.targetHandle === "https" && tn.data.https !== undefined) {
67 return false;
68 }
69 }
70 if (sn.type === "volume") {
71 if (c.targetHandle !== "volume") {
72 return false;
73 }
74 return true;
75 }
76 if (tn.type === "network") {
77 if (c.sourceHandle !== "subdomain") {
78 return false;
79 }
80 if (sn.type !== "gateway-https" && sn.type !== "gateway-tcp") {
81 return false;
82 }
83 }
84 return true;
85 },
86 [flow],
87 );
88 const env = useEnv();
89 useEffect(() => {
90 const networkNodes: AppNode[] = env.networks.map((n) => ({
91 id: n.domain,
92 type: "network",
93 position: {
94 x: 0,
95 y: 0,
96 },
97 isConnectable: true,
98 data: {
99 domain: n.domain,
100 label: n.domain,
101 envVars: [],
102 ports: [],
103 state: "success", // TODO(gio): monitor network health
104 },
105 }));
106 const prevNodes = store.nodes;
107 const newNodes = networkNodes.concat(prevNodes.filter((n) => n.type !== "network"));
108 // TODO(gio): actually compare
109 if (prevNodes.length !== newNodes.length) {
110 store.setNodes(newNodes);
111 }
112 }, [env, store]);
113 return (
114 <div style={{ width: "100%", height: "100%" }}>
115 <ReactFlow
116 nodeTypes={nodeTypes}
117 nodes={nodes}
118 edges={edges}
119 onNodesChange={onNodesChange}
120 onEdgesChange={onEdgesChange}
121 onConnect={onConnect}
122 isValidConnection={isValidConnection}
123 fitView
124 proOptions={{ hideAttribution: true }}
125 >
126 <Controls />
127 <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
128 <Panel position="bottom-right">
129 <Actions />
130 </Panel>
131 </ReactFlow>
132 </div>
133 );
gio5f2f1002025-03-20 18:38:48 +0400134}
135
136export default Canvas;