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;