Canvas: Reuse node details component in overview

Make app details tabular.

Change-Id: I78a641e8e513eec44573bb8c8a391ef81a66e7fe
diff --git a/apps/canvas/front/src/Overview.tsx b/apps/canvas/front/src/Overview.tsx
index a2b35df..4e286ce 100644
--- a/apps/canvas/front/src/Overview.tsx
+++ b/apps/canvas/front/src/Overview.tsx
@@ -1,267 +1,30 @@
-import React, { useCallback, useMemo, useState } from "react";
-import {
-	useStateStore,
-	GithubNode,
-	ServiceNode,
-	GatewayHttpsNode,
-	nodeLabel,
-	Port,
-	nodeEnvVarNames,
-	AppNode,
-} from "@/lib/state";
-import { Button } from "./components/ui/button";
-import { Icon } from "./components/icon";
-import { PlusIcon } from "lucide-react";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "./components/ui/dialog";
-import { Input } from "./components/ui/input";
-import { Label } from "./components/ui/label";
-import { useToast } from "./hooks/use-toast";
-import { v4 as uuidv4 } from "uuid";
+import React, { useMemo } from "react";
+import { useStateStore, ServiceNode } from "@/lib/state";
+import { NodeDetails } from "./components/node-details";
+import { Actions } from "./components/actions";
+import { Canvas } from "./components/canvas";
 
 export function Overview(): React.ReactNode {
-	const nodes = useStateStore((state) => state.nodes);
-	const edges = useStateStore((state) => state.edges);
-	const githubNodes = useMemo(() => nodes.filter((node): node is GithubNode => node.type === "github"), [nodes]);
-	const getServicesForRepo = useCallback(
-		(repoId: string): ServiceNode[] => {
-			return nodes.filter((node): node is ServiceNode => {
-				if (node.type !== "app") return false;
-				return edges.some(
-					(edge) =>
-						edge.source === repoId &&
-						edge.target === node.id &&
-						edge.sourceHandle === "repository" &&
-						edge.targetHandle === "repository",
-				);
-			});
-		},
-		[nodes, edges],
-	);
+	const store = useStateStore();
+	const nodes = useMemo(() => store.nodes, [store.nodes]);
+	const isDeployMode = useMemo(() => store.mode === "deploy", [store.mode]);
 	return (
-		<div className="h-full overflow-auto bg-muted p-4 flex flex-col gap-4">
-			{githubNodes.map((repoNode) => {
-				const services = getServicesForRepo(repoNode.id);
-				return (
-					<div key={repoNode.id}>
-						<h2 className="text-xl font-medium mb-3 flex flex-row items-center gap-2">
-							<Icon type="github" /> {nodeLabel(repoNode)}
-						</h2>
-						{services.length > 0 ? (
-							<ul className="space-y-4">
-								{services.map((serviceNode) => (
-									<li key={serviceNode.id} className="pl-4 border-l-2 border-gray-200">
-										<Service service={serviceNode} />
-									</li>
-								))}
-							</ul>
-						) : (
-							<p className="text-sm text-gray-500 pl-4">No services imported from this repository.</p>
-						)}
-					</div>
-				);
-			})}
-			{nodes
-				.filter((n) => n.type === "volume")
-				.map((n) => {
-					return (
-						<div key={n.id}>
-							<h2 className="text-xl font-medium mb-3 flex flex-row items-center gap-2">
-								<Icon type="volume" /> {nodeLabel(n)}
-							</h2>
-							<div className="pl-4 border-l-2 border-gray-200">
-								<Exports n={n} />
-							</div>
-						</div>
-					);
-				})}
-			{nodes
-				.filter((n) => n.type === "postgresql")
-				.map((n) => {
-					return (
-						<div key={n.id}>
-							<h2 className="text-xl font-medium mb-3 flex flex-row items-center gap-2">
-								<Icon type="postgresql" /> {nodeLabel(n)}
-							</h2>
-							<div className="pl-4 border-l-2 border-gray-200">
-								<Exports n={n} />
-							</div>
-						</div>
-					);
-				})}
-			{nodes
-				.filter((n) => n.type === "mongodb")
-				.map((n) => {
-					return (
-						<div key={n.id}>
-							<h2 className="text-xl font-medium mb-3 flex flex-row items-center gap-2">
-								<Icon type="mongodb" /> {nodeLabel(n)}
-							</h2>
-							<div className="pl-4 border-l-2 border-gray-200">
-								<Exports n={n} />
-							</div>
-						</div>
-					);
-				})}
-		</div>
-	);
-}
-
-function Service({ service: serviceNode }: { service: ServiceNode }): React.ReactNode {
-	const { toast } = useToast();
-	const nodes = useStateStore((state) => state.nodes);
-	const updateNodeData = useStateStore((state) => state.updateNodeData);
-	const [isAddPortModalOpen, setIsAddPortModalOpen] = useState(false);
-	const [newPortName, setNewPortName] = useState("");
-	const [newPortValue, setNewPortValue] = useState("");
-
-	const httpsGateways = useMemo(
-		() => nodes.filter((node): node is GatewayHttpsNode => node.type === "gateway-https"),
-		[nodes],
-	);
-	const getGatewayForServicePort = useCallback(
-		(serviceId: string, port: Port): GatewayHttpsNode[] => {
-			return httpsGateways.filter(
-				(g) => g.data.https?.serviceId === serviceId && g.data.https?.portId === port.id,
-			);
-		},
-		[httpsGateways],
-	);
-	const getGatewayUrl = (g: GatewayHttpsNode): string => {
-		if (g.data.subdomain && g.data.network) {
-			return `https://${g.data.subdomain}.${g.data.network}`;
-		}
-		return "Gateway not fully configured";
-	};
-
-	const handleAddPort = () => {
-		if (!newPortName || !newPortValue) {
-			toast({
-				title: "Port name and value are required.",
-				variant: "destructive",
-			});
-			return;
-		}
-		const portValueNumber = parseInt(newPortValue, 10);
-		if (isNaN(portValueNumber) || portValueNumber <= 0 || portValueNumber > 65535) {
-			toast({
-				title: "Invalid port number.",
-				variant: "destructive",
-			});
-			return;
-		}
-		const newPort: Port = {
-			id: uuidv4(),
-			name: newPortName,
-			value: portValueNumber,
-		};
-		updateNodeData<"app">(serviceNode.id, {
-			ports: [...(serviceNode.data.ports || []), newPort],
-		} as Partial<ServiceNode["data"]>);
-		setNewPortName("");
-		setNewPortValue("");
-		setIsAddPortModalOpen(false);
-	};
-
-	return (
-		<>
-			<h3 className="text-lg font-medium text-gray-700 flex flex-row items-center gap-2">
-				<Icon type="app" /> {nodeLabel(serviceNode)}
-			</h3>
-			<div className="text-sm text-gray-500 pl-4 flex flex-row items-center gap-2">
-				<div>Branch: {serviceNode.data.repository?.branch ?? "master"}</div>
-				<div>Location: {serviceNode.data.repository?.rootDir ?? "/"}</div>
+		<div className="h-full w-full overflow-auto bg-white p-2">
+			<div className="w-full flex flex-row justify-end">
+				<Actions />
+				<Canvas className="hidden" />
 			</div>
-			<div className="pl-4">
-				<h4 className="text-sm font-medium text-gray-500 flex flex-row items-center gap-2">
-					Ports
-					<Button variant="ghost" size="icon" onClick={() => setIsAddPortModalOpen(true)}>
-						<PlusIcon />
-					</Button>
-				</h4>
-				<ul className="pl-2">
-					{(serviceNode.data.ports || []).map((port) => {
-						const gateways = getGatewayForServicePort(serviceNode.id, port);
+			<div className="flex flex-wrap gap-4 pt-2">
+				{nodes
+					.filter((n): n is ServiceNode => n.type === "app")
+					.map((n) => {
 						return (
-							<li key={port.id} className="text-sm text-gray-600">
-								<span className="font-medium">{port.name.toUpperCase()}:</span> {port.value}
-								{gateways.map((g) => (
-									<Button variant="link" asChild key={g.id} className="!h-fit !py-0">
-										<a href={getGatewayUrl(g)} target="_blank" rel="noopener noreferrer">
-											{getGatewayUrl(g)}
-										</a>
-									</Button>
-								))}
-							</li>
+							<div key={n.id} className="h-fit w-fit rounded-lg border-gray-200 border-2 p-2">
+								<NodeDetails disabled={isDeployMode} {...n} />
+							</div>
 						);
 					})}
-				</ul>
 			</div>
-			<div className="pl-4">
-				<Exports n={serviceNode} />
-			</div>
-			<Dialog open={isAddPortModalOpen} onOpenChange={setIsAddPortModalOpen}>
-				<DialogContent>
-					<DialogHeader>
-						<DialogTitle>Add New Port to {nodeLabel(serviceNode)}</DialogTitle>
-					</DialogHeader>
-					<div>
-						<div>
-							<Label htmlFor="portName">Name</Label>
-							<Input
-								id="portName"
-								value={newPortName}
-								onChange={(e) => setNewPortName(e.target.value)}
-								placeholder="e.g., HTTP, Admin"
-							/>
-						</div>
-						<div>
-							<Label htmlFor="portValue">Port Number</Label>
-							<Input
-								id="portValue"
-								type="number"
-								value={newPortValue}
-								onChange={(e) => setNewPortValue(e.target.value)}
-								placeholder="e.g., 80, 8080"
-							/>
-						</div>
-					</div>
-					<DialogFooter>
-						<DialogClose asChild>
-							<Button variant="outline">Cancel</Button>
-						</DialogClose>
-						<Button onClick={handleAddPort}>Add Port</Button>
-					</DialogFooter>
-				</DialogContent>
-			</Dialog>
-		</>
-	);
-}
-
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/ui/accordion";
-import { Badge } from "./components/ui/badge";
-
-function Exports({ n }: { n: AppNode }): React.ReactNode {
-	return (
-		<Accordion type="single" collapsible className="w-full">
-			<AccordionItem value="exports" className="!border-none">
-				<AccordionTrigger className="flex flex-row-reverse !gap-1 !justify-end !h-fit !py-1">
-					<Badge className="h-5 min-w-5 rounded-full px-2 font-mono tabular-nums">
-						{nodeEnvVarNames(n).length}
-					</Badge>{" "}
-					Exports
-				</AccordionTrigger>
-				<AccordionContent>
-					<ul className="pl-2 space-y-1">
-						{nodeEnvVarNames(n).map((name) => {
-							return (
-								<li key={name} className="text-xs font-mono">
-									{name}
-								</li>
-							);
-						})}
-					</ul>
-				</AccordionContent>
-			</AccordionItem>
-		</Accordion>
+		</div>
 	);
 }