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/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index f283ec8..3047fdf 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -26,11 +26,7 @@
 	}
 }
 
-interface ActionsProps {
-	isOverview?: boolean;
-}
-
-export function Actions({ isOverview = false }: ActionsProps) {
+export function Actions() {
 	const { toast } = useToast();
 	const store = useStateStore();
 	const projectId = useProjectId();
@@ -275,6 +271,12 @@
 	if (store.mode === "deploy") {
 		return (
 			<div className="flex flex-row gap-1 items-center">
+				<Button
+					onClick={() => store.setBuildMode(store.buildMode === "overview" ? "canvas" : "overview")}
+					{...reloadProps}
+				>
+					{store.buildMode === "overview" ? "Canvas" : "Overview"}
+				</Button>
 				<Button onClick={edit} {...reloadProps}>
 					Edit
 				</Button>
@@ -323,12 +325,13 @@
 		return (
 			<>
 				<div className="flex flex-row gap-1 items-center">
-					{isOverview && (
-						<Button onClick={() => setShowResourcesModal(true)}>
-							<Plus className="w-4 h-4 mr-1" />
-							Add
-						</Button>
-					)}
+					<Button onClick={() => store.setBuildMode(store.buildMode === "overview" ? "canvas" : "overview")}>
+						{store.buildMode === "overview" ? "Canvas" : "Overview"}
+					</Button>
+					<Button onClick={() => setShowResourcesModal(true)}>
+						<Plus className="w-4 h-4 mr-1" />
+						Add
+					</Button>
 					<Button onClick={deploy} {...deployProps}>
 						{deployProps.loading ? (
 							<>
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
deleted file mode 100644
index 2d78abd..0000000
--- a/apps/canvas/front/src/components/canvas.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-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 "./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";
-import { NodeNetwork } from "./node-network";
-import { AppNode } from "config";
-
-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 (
-		<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;