Canvas: Update layout
Combine separate Overview and Canvas tabs into one Build tab
Add Overview <-> Canvas switcher to Actions
Change-Id: I40f7742be587b475ae6e88af2bcf9cae34f93168
diff --git a/apps/canvas/front/src/Canvas.tsx b/apps/canvas/front/src/Canvas.tsx
index eaaaf70..ba17153 100644
--- a/apps/canvas/front/src/Canvas.tsx
+++ b/apps/canvas/front/src/Canvas.tsx
@@ -1,41 +1,132 @@
-import { Resources } from "./components/resources";
-import { Canvas } from "./components/canvas";
-import { Details } from "./Details";
-import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./components/ui/resizable";
-import { Tools } from "./Tools";
-import { useStateStore } from "./lib/state";
+import "@xyflow/react/dist/style.css";
+import {
+ ReactFlow,
+ Background,
+ Controls,
+ Connection,
+ BackgroundVariant,
+ Edge,
+ useReactFlow,
+ Panel,
+ useStoreApi,
+} from "@xyflow/react";
+import { useStateStore, AppState, useZoom } from "@/lib/state";
+import { useShallow } from "zustand/react/shallow";
+import { useCallback, useEffect, useMemo } from "react";
+import { NodeGatewayHttps } from "@/components/node-gateway-https";
+import { NodeApp } from "@/components/node-app";
+import { NodeVolume } from "./components/node-volume";
+import { NodePostgreSQL } from "./components/node-postgresql";
+import { NodeMongoDB } from "./components/node-mongodb";
+import { NodeGithub } from "./components/node-github";
+import { Actions } from "./components/actions";
+import { NodeGatewayTCP } from "./components/node-gateway-tcp";
+import { NodeNetwork } from "./components/node-network";
+import { AppNode } from "config";
-export function CanvasBuilder() {
+const selector = (state: AppState) => ({
+ nodes: state.nodes,
+ edges: state.edges,
+ onNodesChange: state.onNodesChange,
+ onEdgesChange: state.onEdgesChange,
+ onConnect: state.onConnect,
+});
+
+export function Canvas({ className }: { className?: string }) {
+ const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(useShallow(selector));
const store = useStateStore();
+ const instance = useReactFlow();
+ const flow = useStoreApi();
+ const viewport = useZoom();
+ useEffect(() => {
+ store.setViewport({
+ width: flow.getState().width,
+ height: flow.getState().height,
+ transformX: flow.getState().transform[0],
+ transformY: flow.getState().transform[1],
+ transformZoom: flow.getState().transform[2],
+ });
+ }, [store, flow]);
+ useEffect(() => {
+ instance.setViewport(viewport);
+ }, [viewport, instance]);
+ const nodeTypes = useMemo(
+ () => ({
+ network: NodeNetwork,
+ 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 = instance.getNode(c.source)! as AppNode;
+ const tn = instance.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;
+ }
+ if (tn.type === "network") {
+ if (c.sourceHandle !== "subdomain") {
+ return false;
+ }
+ if (sn.type !== "gateway-https" && sn.type !== "gateway-tcp") {
+ return false;
+ }
+ }
+ return true;
+ },
+ [instance],
+ );
return (
- <ResizablePanelGroup direction="horizontal" className="w-full h-full">
- <ResizablePanel defaultSize={80}>
- <ResizablePanelGroup direction="vertical">
- <ResizablePanel defaultSize={80}>
- <ResizablePanelGroup direction="horizontal">
- {store.mode === "edit" && (
- <>
- <ResizablePanel defaultSize={15}>
- <Resources />
- </ResizablePanel>
- <ResizableHandle withHandle />
- </>
- )}
- <ResizablePanel defaultSize={store.mode === "edit" ? 85 : 100}>
- <Canvas />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
- <ResizableHandle withHandle />
- <ResizablePanel defaultSize={20}>
- <Tools />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
- <ResizableHandle withHandle />
- <ResizablePanel defaultSize={20} className="!overflow-y-auto !overflow-x-hidden">
- <Details />
- </ResizablePanel>
- </ResizablePanelGroup>
+ <div style={{ width: "100%", height: "100%" }} className={className}>
+ <ReactFlow
+ nodeTypes={nodeTypes}
+ nodes={nodes}
+ edges={edges}
+ onNodesChange={onNodesChange}
+ onEdgesChange={onEdgesChange}
+ onConnect={onConnect}
+ isValidConnection={isValidConnection}
+ fitView
+ proOptions={{ hideAttribution: true }}
+ >
+ <Controls showInteractive={false} />
+ <Background
+ variant={store.mode === "deploy" ? BackgroundVariant.Dots : BackgroundVariant.Lines}
+ gap={12}
+ size={1}
+ />
+ <Panel position="top-right">
+ <Actions />
+ </Panel>
+ </ReactFlow>
+ </div>
);
}
+
+export default Canvas;