Canvas: build application infrastructure with drag and drop

Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/components/canvas.tsx b/apps/canvas/src/components/canvas.tsx
new file mode 100644
index 0000000..406f2e9
--- /dev/null
+++ b/apps/canvas/src/components/canvas.tsx
@@ -0,0 +1,87 @@
+import '@xyflow/react/dist/style.css';
+import { ReactFlow, Background, Controls, Connection, BackgroundVariant, Edge, useReactFlow, Panel } from '@xyflow/react';
+import { useStateStore, AppState, AppNode } from '@/lib/state';
+import { useShallow } from "zustand/react/shallow";
+import { useCallback, useMemo } from 'react';
+import { NodeGatewayHttps } from "@/components/node-gateway-https";
+import { NodeApp } from '@/components/node-app';
+import { NodeVolume } from './node-volume';
+import { NodePostgreSQL } from './node-postgresql';
+import { NodeMongoDB } from './node-mongodb';
+import { NodeGithub } from './node-github';
+import { Actions } from './actions';
+import { NodeGatewayTCP } from './node-gateway-tcp';
+
+const selector = (state: AppState) => ({
+    nodes: state.nodes,
+    edges: state.edges,
+    onNodesChange: state.onNodesChange,
+    onEdgesChange: state.onEdgesChange,
+    onConnect: state.onConnect,
+});
+
+export function Canvas() {
+    const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(
+        useShallow(selector),
+    );
+    const flow = useReactFlow();
+    const nodeTypes = useMemo(() => ({
+        "app": NodeApp,
+        "gateway-https": NodeGatewayHttps,
+        "gateway-tcp": NodeGatewayTCP,
+        "volume": NodeVolume,
+        "postgresql": NodePostgreSQL,
+        "mongodb": NodeMongoDB,
+        "github": NodeGithub,
+    }), []);
+    const isValidConnection = useCallback((c: Edge | Connection) => {
+        if (c.source === c.target) {
+            return false;
+        }
+        const sn = flow.getNode(c.source)! as AppNode;
+        const tn = flow.getNode(c.target)! as AppNode;
+        if (sn.type === "github") {
+            return c.targetHandle === "repository";
+        }
+        if (sn.type === "app") {
+              if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
+                return false;
+              }
+        }
+        if (tn.type === "gateway-https") {
+            if (c.targetHandle === "https" && tn.data.https !== undefined) {
+                return false;
+            }
+        }
+        if (sn.type === "volume") {
+            if (c.targetHandle !== "volume") {
+                return false;
+            }
+            return true;
+        }
+        return true;
+    }, [flow]);
+    return (
+        <div style={{ width: '100%', height: '100%' }}>
+            <ReactFlow
+                nodeTypes={nodeTypes}
+                nodes={nodes}
+                edges={edges}
+                onNodesChange={onNodesChange}
+                onEdgesChange={onEdgesChange}
+                onConnect={onConnect}
+                isValidConnection={isValidConnection}
+                fitView
+                proOptions={{ hideAttribution: true }}
+                >
+                <Controls />
+                <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
+                <Panel position="bottom-right">
+                    <Actions />
+                </Panel>
+            </ReactFlow>
+        </div>
+    );
+}
+
+export default Canvas;