blob: 8898b58c06c3ca1eaea1bd6e1b381b39ec90037c [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import '@xyflow/react/dist/style.css';
2import { ReactFlow, Background, Controls, Connection, BackgroundVariant, Edge, useReactFlow, Panel } from '@xyflow/react';
gioaba9a962025-04-25 14:19:40 +00003import { useStateStore, AppState, AppNode, useEnv } from '@/lib/state';
gio5f2f1002025-03-20 18:38:48 +04004import { useShallow } from "zustand/react/shallow";
gioaba9a962025-04-25 14:19:40 +00005import { useCallback, useEffect, useMemo } from 'react';
gio5f2f1002025-03-20 18:38:48 +04006import { NodeGatewayHttps } from "@/components/node-gateway-https";
7import { NodeApp } from '@/components/node-app';
8import { NodeVolume } from './node-volume';
9import { NodePostgreSQL } from './node-postgresql';
10import { NodeMongoDB } from './node-mongodb';
11import { NodeGithub } from './node-github';
12import { Actions } from './actions';
13import { NodeGatewayTCP } from './node-gateway-tcp';
gioaba9a962025-04-25 14:19:40 +000014import { NodeNetwork } from './node-network';
gio5f2f1002025-03-20 18:38:48 +040015
16const selector = (state: AppState) => ({
17 nodes: state.nodes,
18 edges: state.edges,
19 onNodesChange: state.onNodesChange,
20 onEdgesChange: state.onEdgesChange,
21 onConnect: state.onConnect,
22});
23
24export function Canvas() {
25 const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(
26 useShallow(selector),
27 );
gioaba9a962025-04-25 14:19:40 +000028 const store = useStateStore();
gio5f2f1002025-03-20 18:38:48 +040029 const flow = useReactFlow();
30 const nodeTypes = useMemo(() => ({
gioaba9a962025-04-25 14:19:40 +000031 "network": NodeNetwork,
gio5f2f1002025-03-20 18:38:48 +040032 "app": NodeApp,
33 "gateway-https": NodeGatewayHttps,
34 "gateway-tcp": NodeGatewayTCP,
35 "volume": NodeVolume,
36 "postgresql": NodePostgreSQL,
37 "mongodb": NodeMongoDB,
38 "github": NodeGithub,
39 }), []);
40 const isValidConnection = useCallback((c: Edge | Connection) => {
41 if (c.source === c.target) {
42 return false;
43 }
44 const sn = flow.getNode(c.source)! as AppNode;
45 const tn = flow.getNode(c.target)! as AppNode;
46 if (sn.type === "github") {
47 return c.targetHandle === "repository";
48 }
49 if (sn.type === "app") {
gio7f98e772025-05-07 11:00:14 +000050 if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
gio5f2f1002025-03-20 18:38:48 +040051 return false;
gio7f98e772025-05-07 11:00:14 +000052 }
gio5f2f1002025-03-20 18:38:48 +040053 }
54 if (tn.type === "gateway-https") {
55 if (c.targetHandle === "https" && tn.data.https !== undefined) {
56 return false;
57 }
58 }
59 if (sn.type === "volume") {
60 if (c.targetHandle !== "volume") {
61 return false;
62 }
63 return true;
64 }
gioaba9a962025-04-25 14:19:40 +000065 if (tn.type === "network") {
66 if (c.sourceHandle !== "subdomain") {
67 return false;
68 }
69 if (sn.type !== "gateway-https" && sn.type !== "gateway-tcp") {
70 return false;
71 }
72 }
gio5f2f1002025-03-20 18:38:48 +040073 return true;
74 }, [flow]);
gioaba9a962025-04-25 14:19:40 +000075 const env = useEnv();
76 useEffect(() => {
77 const networkNodes: AppNode[] = env.networks.map((n) => ({
78 id: n.domain,
79 type: "network",
80 position: {
81 x: 0,
82 y: 0,
83 },
gio7f98e772025-05-07 11:00:14 +000084 isConnectable: true,
gioaba9a962025-04-25 14:19:40 +000085 data: {
86 domain: n.domain,
87 label: n.domain,
88 envVars: [],
89 ports: [],
gioda708652025-04-30 14:57:38 +040090 state: "success", // TODO(gio): monitor network health
gioaba9a962025-04-25 14:19:40 +000091 },
92 }));
93 const prevNodes = store.nodes;
94 const newNodes = networkNodes.concat(prevNodes.filter((n) => n.type !== "network"));
95 // TODO(gio): actually compare
96 if (prevNodes.length !== newNodes.length) {
97 store.setNodes(newNodes);
98 }
99 }, [env, store]);
gio5f2f1002025-03-20 18:38:48 +0400100 return (
101 <div style={{ width: '100%', height: '100%' }}>
102 <ReactFlow
103 nodeTypes={nodeTypes}
104 nodes={nodes}
105 edges={edges}
106 onNodesChange={onNodesChange}
107 onEdgesChange={onEdgesChange}
108 onConnect={onConnect}
109 isValidConnection={isValidConnection}
110 fitView
111 proOptions={{ hideAttribution: true }}
gio7f98e772025-05-07 11:00:14 +0000112 >
gio5f2f1002025-03-20 18:38:48 +0400113 <Controls />
114 <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
115 <Panel position="bottom-right">
116 <Actions />
117 </Panel>
118 </ReactFlow>
119 </div>
120 );
121}
122
123export default Canvas;