Canvas: Reuse node details component in overview

Make app details tabular.

Change-Id: I78a641e8e513eec44573bb8c8a391ef81a66e7fe
diff --git a/apps/canvas/front/src/Canvas.tsx b/apps/canvas/front/src/Canvas.tsx
index ae7e137..f8a4b4d 100644
--- a/apps/canvas/front/src/Canvas.tsx
+++ b/apps/canvas/front/src/Canvas.tsx
@@ -33,7 +33,7 @@
 				</ResizablePanelGroup>
 			</ResizablePanel>
 			<ResizableHandle withHandle />
-			<ResizablePanel defaultSize={20} className="!overflow-y-auto">
+			<ResizablePanel defaultSize={20} className="!overflow-y-auto !overflow-x-hidden">
 				<Details />
 			</ResizablePanel>
 		</ResizablePanelGroup>
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>
 	);
 }
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index dbb5ea6..d87f458 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -11,7 +11,7 @@
 	DropdownMenuContent,
 	DropdownMenuTrigger,
 } from "./ui/dropdown-menu";
-import { LoaderCircle, Menu } from "lucide-react";
+import { Ellipsis, LoaderCircle } from "lucide-react";
 
 function toNodeType(t: string): string {
 	if (t === "ingress") {
@@ -70,7 +70,6 @@
 				return;
 			}
 			const data: { type: string; name: string; status: string }[] = await resp.json();
-			console.log(data);
 			for (const n of nodes) {
 				if (n.type === "network") {
 					continue;
@@ -269,7 +268,9 @@
 				</Button>
 				<DropdownMenu>
 					<DropdownMenuTrigger>
-						<Menu className="rounded-md bg-gray-200 opacity-50" />
+						<Button size="icon">
+							<Ellipsis />
+						</Button>
 					</DropdownMenuTrigger>
 					<DropdownMenuContent className="w-56">
 						<DropdownMenuGroup>
@@ -322,7 +323,9 @@
 				<Button onClick={save}>Save</Button>
 				<DropdownMenu>
 					<DropdownMenuTrigger>
-						<Menu />
+						<Button size="icon">
+							<Ellipsis />
+						</Button>
 					</DropdownMenuTrigger>
 					<DropdownMenuContent className="w-56">
 						<DropdownMenuGroup>
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 861437f..96b8f03 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -31,7 +31,7 @@
 	onConnect: state.onConnect,
 });
 
-export function Canvas() {
+export function Canvas({ className }: { className?: string }) {
 	const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStateStore(useShallow(selector));
 	const store = useStateStore();
 	const instance = useReactFlow();
@@ -101,7 +101,7 @@
 		[instance],
 	);
 	return (
-		<div style={{ width: "100%", height: "100%" }}>
+		<div style={{ width: "100%", height: "100%" }} className={className}>
 			<ReactFlow
 				nodeTypes={nodeTypes}
 				nodes={nodes}
@@ -119,7 +119,7 @@
 					gap={12}
 					size={1}
 				/>
-				<Panel position="bottom-right">
+				<Panel position="top-right">
 					<Actions />
 				</Panel>
 			</ReactFlow>
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 7516d19..71fc358 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -13,21 +13,26 @@
 	AppNode,
 	GithubNode,
 	useEnv,
+	useGithubRepositories,
 } from "@/lib/state";
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
 import { z } from "zod";
-import { DeepPartial, EventType, useForm, ControllerRenderProps, FieldPath } from "react-hook-form";
+import { useForm, EventType, DeepPartial } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
 import { Button } from "./ui/button";
 import { Handle, Position, useNodes } from "@xyflow/react";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
-import { PencilIcon, XIcon } from "lucide-react";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
 import { Textarea } from "./ui/textarea";
 import { Input } from "./ui/input";
-import { Checkbox } from "./ui/checkbox";
+import { Switch } from "./ui/switch";
 import { Label } from "./ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
+import { Code, Container, Network, Pencil, Variable } from "lucide-react";
+import { Icon } from "./icon";
+import { Badge } from "./ui/badge";
+import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./ui/accordion";
 
 export function NodeApp(node: ServiceNode) {
 	const { id, selected } = node;
@@ -71,11 +76,6 @@
 	type: z.enum(ServiceTypes),
 });
 
-const portSchema = z.object({
-	name: z.string().min(1, "required"),
-	value: z.coerce.number().gt(0, "must be positive").lte(65535, "must be less than 65535"),
-});
-
 const sourceSchema = z.object({
 	id: z.string().min(1, "required"),
 	branch: z.string(),
@@ -91,10 +91,124 @@
 	subdomain: z.string().min(1, "required"),
 });
 
-export function NodeAppDetails({ id, data, disabled }: ServiceNode & { disabled?: boolean }) {
+export function NodeAppDetails({ node, disabled }: { node: ServiceNode; disabled?: boolean }) {
+	const { data } = node;
+	return (
+		<>
+			<Name node={node} disabled={disabled} />
+			<Tabs defaultValue="runtime">
+				<TabsList className="w-full flex flex-row justify-between">
+					<TabsTrigger value="runtime">
+						<TooltipProvider>
+							<Tooltip>
+								<TooltipTrigger>
+									<Container />
+								</TooltipTrigger>
+								<TooltipContent>Runtime</TooltipContent>
+							</Tooltip>
+						</TooltipProvider>
+					</TabsTrigger>
+					<TabsTrigger value="ports">
+						<TooltipProvider>
+							<Tooltip>
+								<TooltipTrigger className="flex flex-row gap-1 items-center">
+									<Network />
+								</TooltipTrigger>
+								<TooltipContent>
+									Ports{" "}
+									<Badge variant="secondary" className="rounded-full">
+										{data.ports?.length ?? 0}
+									</Badge>
+								</TooltipContent>
+							</Tooltip>
+						</TooltipProvider>
+					</TabsTrigger>
+					<TabsTrigger value="vars">
+						<TooltipProvider>
+							<Tooltip>
+								<TooltipTrigger className="flex flex-row gap-1 items-center">
+									<Variable />
+								</TooltipTrigger>
+								<TooltipContent>
+									Variables{" "}
+									<Badge variant="secondary" className="rounded-full">
+										{data.envVars?.length ?? 0}
+									</Badge>
+								</TooltipContent>
+							</Tooltip>
+						</TooltipProvider>
+					</TabsTrigger>
+					<TabsTrigger value="dev">
+						<TooltipProvider>
+							<Tooltip>
+								<TooltipTrigger className="flex flex-row gap-1 items-center">
+									<Code />
+								</TooltipTrigger>
+								<TooltipContent>Dev</TooltipContent>
+							</Tooltip>
+						</TooltipProvider>
+					</TabsTrigger>
+				</TabsList>
+				<TabsContent value="runtime">
+					<Runtime node={node} disabled={disabled} />
+				</TabsContent>
+				<TabsContent value="ports">
+					<Ports node={node} disabled={disabled} />
+				</TabsContent>
+				<TabsContent value="vars">
+					<EnvVars node={node} disabled={disabled} />
+				</TabsContent>
+				<TabsContent value="dev">
+					<Dev node={node} disabled={disabled} />
+				</TabsContent>
+			</Tabs>
+		</>
+	);
+}
+
+function Name({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
 	const store = useStateStore();
-	const nodes = useNodes<AppNode>();
-	const env = useEnv();
+	const [isEditing, setIsEditing] = useState(false);
+	useEffect(() => {
+		if (data.label === "" && !disabled) {
+			setIsEditing(true);
+		}
+	}, [data.label, disabled]);
+	return (
+		<div className="flex flex-row gap-1 items-center">
+			<Icon type="app" />
+			{isEditing ? (
+				<Input
+					placeholder="Name"
+					value={data.label}
+					onChange={(e) => store.updateNodeData(id, { label: e.target.value })}
+					onBlur={() => {
+						if (data.label !== "") {
+							setIsEditing(false);
+						}
+					}}
+					autoFocus={true}
+				/>
+			) : (
+				<h3
+					className="text-lg font-bold cursor-text select-none hover:outline-solid hover:outline-2 hover:outline-gray-200"
+					onClick={() => {
+						if (!disabled) {
+							setIsEditing(true);
+						}
+					}}
+				>
+					{data.label}
+				</h3>
+			)}
+		</div>
+	);
+}
+
+function Runtime({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
+	const store = useStateStore();
 	const form = useForm<z.infer<typeof schema>>({
 		resolver: zodResolver(schema),
 		mode: "onChange",
@@ -103,41 +217,12 @@
 			type: data.type,
 		},
 	});
-	const portForm = useForm<z.infer<typeof portSchema>>({
-		resolver: zodResolver(portSchema),
-		mode: "onSubmit",
-		defaultValues: {
-			name: "",
-			value: 0,
-		},
-	});
-	const onSubmit = useCallback(
-		(values: z.infer<typeof portSchema>) => {
-			const portId = uuidv4();
-			store.updateNodeData<"app">(id, {
-				ports: (data.ports || []).concat({
-					id: portId,
-					name: values.name.toLowerCase(),
-					value: values.value,
-				}),
-				envVars: (data.envVars || []).concat({
-					id: uuidv4(),
-					source: null,
-					portId,
-					name: `DODO_PORT_${values.name.toUpperCase()}`,
-				}),
-			});
-			portForm.reset();
-		},
-		[id, data, portForm, store],
-	);
 	useEffect(() => {
 		const sub = form.watch(
 			(
 				value: DeepPartial<z.infer<typeof schema>>,
 				{ name, type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
 			) => {
-				console.log({ name, type });
 				if (type !== "change") {
 					return;
 				}
@@ -163,21 +248,6 @@
 		);
 		return () => sub.unsubscribe();
 	}, [id, form, store]);
-	const focus = useCallback(
-		(field: ControllerRenderProps<z.infer<typeof schema>, FieldPath<z.infer<typeof schema>>>, name: string) => {
-			return (e: HTMLElement | null) => {
-				field.ref(e);
-				if (e != null && name === data.activeField) {
-					console.log(e);
-					e.focus();
-					store.updateNodeData(id, {
-						activeField: undefined,
-					});
-				}
-			};
-		},
-		[id, data, store],
-	);
 	const [typeProps, setTypeProps] = useState({});
 	useEffect(() => {
 		if (data.activeField === "type") {
@@ -189,82 +259,88 @@
 			setTypeProps({});
 		}
 	}, [id, data, store, setTypeProps]);
-	const editAlias = useCallback(
-		(e: BoundEnvVar) => {
-			return () => {
-				store.updateNodeData(id, {
-					...data,
-					envVars: data.envVars!.map((o) => {
-						if (o.id !== e.id) {
-							return o;
-						} else
-							return {
-								...o,
-								isEditting: true,
-							};
-					}),
-				});
-			};
-		},
-		[id, data, store],
-	);
-	const saveAlias = useCallback(
-		(e: BoundEnvVar, value: string, store: AppState) => {
-			store.updateNodeData(id, {
-				...data,
-				envVars: data.envVars!.map((o) => {
-					if (o.id !== e.id) {
-						return o;
-					}
-					if (value) {
-						return {
-							...o,
-							isEditting: false,
-							alias: value.toUpperCase(),
-						};
-					}
-					console.log(o);
-					if ("alias" in o) {
-						const { alias: _, ...rest } = o;
-						console.log(rest);
-						return {
-							...rest,
-							isEditting: false,
-						};
-					}
-					return {
-						...o,
-						isEditting: false,
-					};
-				}),
+	const setPreBuildCommands = useCallback(
+		(e: React.ChangeEvent<HTMLTextAreaElement>) => {
+			store.updateNodeData<"app">(id, {
+				preBuildCommands: e.currentTarget.value,
 			});
 		},
-		[id, data],
+		[id, store],
 	);
-	const saveAliasOnEnter = useCallback(
-		(e: BoundEnvVar) => {
-			return (event: KeyboardEvent<HTMLInputElement>) => {
-				if (event.key === "Enter") {
-					event.preventDefault();
-					saveAlias(e, event.currentTarget.value, store);
-				}
-			};
-		},
-		[store, saveAlias],
+	return (
+		<>
+			<SourceRepo node={node} disabled={disabled} />
+			<Form {...form}>
+				<form className="space-y-2">
+					<Label>Container Image</Label>
+					<FormField
+						control={form.control}
+						name="type"
+						render={({ field }) => (
+							<FormItem>
+								<Select
+									onValueChange={field.onChange}
+									value={field.value || ""}
+									{...typeProps}
+									disabled={disabled}
+								>
+									<FormControl>
+										<SelectTrigger>
+											<SelectValue />
+										</SelectTrigger>
+									</FormControl>
+									<SelectContent>
+										{ServiceTypes.map((t) => (
+											<SelectItem key={t} value={t}>
+												{t}
+											</SelectItem>
+										))}
+									</SelectContent>
+								</Select>
+								<FormMessage />
+							</FormItem>
+						)}
+					/>
+				</form>
+			</Form>
+			<Label>Pre-Build Commands</Label>
+			<Textarea
+				placeholder="new line separated list of commands to run before running the service"
+				value={data.preBuildCommands}
+				onChange={setPreBuildCommands}
+				disabled={disabled}
+			/>
+		</>
 	);
-	const saveAliasOnBlur = useCallback(
-		(e: BoundEnvVar) => {
-			return (event: FocusEvent<HTMLInputElement>) => {
-				saveAlias(e, event.currentTarget.value, store);
-			};
-		},
-		[store, saveAlias],
-	);
+}
+
+function Ports({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
+	const store = useStateStore();
+	const [name, setName] = useState("");
+	const [value, setValue] = useState("");
+	const onSubmit = useCallback(() => {
+		const portId = uuidv4();
+		store.updateNodeData<"app">(id, {
+			ports: (data.ports || []).concat({
+				id: portId,
+				name: name.toUpperCase(),
+				value: Number(value),
+			}),
+			envVars: (data.envVars || []).concat({
+				id: uuidv4(),
+				source: null,
+				portId,
+				name: `DODO_PORT_${name.toUpperCase()}`,
+			}),
+		});
+		setName("");
+		setValue("");
+	}, [id, data, store, name, value, setName, setValue]);
 	const removePort = useCallback(
 		(portId: string) => {
 			// TODO(gio): this is ugly
 			const tcpRemoved = new Set<string>();
-			console.log(store.edges);
 			store.setEdges(
 				store.edges.filter((e) => {
 					if (e.source !== id || e.sourceHandle !== "ports") {
@@ -350,76 +426,174 @@
 		},
 		[id, data, store],
 	);
-	const setPreBuildCommands = useCallback(
-		(e: React.ChangeEvent<HTMLTextAreaElement>) => {
-			store.updateNodeData<"app">(id, {
-				preBuildCommands: e.currentTarget.value,
+	return (
+		<div className="flex flex-col gap-1">
+			<div className="grid grid-cols-[1fr_1fr_auto] gap-1">
+				{data &&
+					data.ports &&
+					data.ports.map((p) => (
+						<>
+							<div className="flex items-center px-3">{p.name.toUpperCase()}</div>
+							<div className="flex items-center px-3">{p.value}</div>
+							<div className="flex items-center">
+								<Button
+									variant="destructive"
+									className="w-full"
+									onClick={() => removePort(p.id)}
+									disabled={disabled}
+								>
+									Remove
+								</Button>
+							</div>
+						</>
+					))}
+				<div>
+					<Input
+						placeholder="name"
+						className="uppercase w-0 min-w-full"
+						disabled={disabled}
+						value={name}
+						onChange={(e) => setName(e.target.value)}
+					/>
+				</div>
+				<div>
+					<Input
+						placeholder="0"
+						className="w-0 min-w-full"
+						disabled={disabled}
+						value={value}
+						onChange={(e) => setValue(e.target.value)}
+					/>
+				</div>
+				<div>
+					<Button type="submit" className="w-full" disabled={disabled} onClick={onSubmit}>
+						Add
+					</Button>
+				</div>
+			</div>
+		</div>
+	);
+}
+
+function EnvVars({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
+	const store = useStateStore();
+	const editAlias = useCallback(
+		(e: BoundEnvVar) => {
+			return () => {
+				store.updateNodeData(id, {
+					...data,
+					envVars: data.envVars!.map((o) => {
+						if (o.id !== e.id) {
+							return o;
+						} else
+							return {
+								...o,
+								isEditting: true,
+							};
+					}),
+				});
+			};
+		},
+		[id, data, store],
+	);
+	const saveAlias = useCallback(
+		(e: BoundEnvVar, value: string, store: AppState) => {
+			store.updateNodeData(id, {
+				...data,
+				envVars: data.envVars!.map((o) => {
+					if (o.id !== e.id) {
+						return o;
+					}
+					if (value) {
+						return {
+							...o,
+							isEditting: false,
+							alias: value.toUpperCase(),
+						};
+					}
+					if ("alias" in o) {
+						const { alias: _, ...rest } = o;
+						return {
+							...rest,
+							isEditting: false,
+						};
+					}
+					return {
+						...o,
+						isEditting: false,
+					};
+				}),
 			});
 		},
-		[id, store],
+		[id, data],
 	);
-
-	const sourceForm = useForm<z.infer<typeof sourceSchema>>({
-		resolver: zodResolver(sourceSchema),
-		mode: "onChange",
-		defaultValues: {
-			id: data?.repository?.id,
-			branch: data.repository && "branch" in data.repository ? data.repository.branch : undefined,
-			rootDir: data.repository && "rootDir" in data.repository ? data.repository.rootDir : undefined,
-		},
-	});
-	useEffect(() => {
-		const sub = sourceForm.watch(
-			(
-				value: DeepPartial<z.infer<typeof sourceSchema>>,
-				{ name }: { name?: keyof z.infer<typeof sourceSchema> | undefined; type?: EventType | undefined },
-			) => {
-				console.log(value);
-				if (name === "id") {
-					let edges = store.edges;
-					if (data?.repository?.id !== undefined) {
-						edges = edges.filter((e) => {
-							if (e.target === id && e.targetHandle === "repository" && e.source === data.repository.id) {
-								return false;
-							} else {
-								return true;
-							}
-						});
-					}
-					if (value.id !== undefined) {
-						edges = edges.concat({
-							id: uuidv4(),
-							source: value.id,
-							sourceHandle: "repository",
-							target: id,
-							targetHandle: "repository",
-						});
-					}
-					store.setEdges(edges);
-					store.updateNodeData<"app">(id, {
-						repository: {
-							id: value.id,
-						},
-					});
-				} else if (name === "branch") {
-					store.updateNodeData<"app">(id, {
-						repository: {
-							...data?.repository,
-							branch: value.branch,
-						},
-					});
-				} else if (name === "rootDir") {
-					store.updateNodeData<"app">(id, {
-						repository: {
-							...data?.repository,
-							rootDir: value.rootDir,
-						},
-					});
+	const saveAliasOnEnter = useCallback(
+		(e: BoundEnvVar) => {
+			return (event: KeyboardEvent<HTMLInputElement>) => {
+				if (event.key === "Enter") {
+					event.preventDefault();
+					saveAlias(e, event.currentTarget.value, store);
 				}
-			},
-		);
-		return () => sub.unsubscribe();
-	}, [id, data, sourceForm, store]);
+			};
+		},
+		[store, saveAlias],
+	);
+	const saveAliasOnBlur = useCallback(
+		(e: BoundEnvVar) => {
+			return (event: FocusEvent<HTMLInputElement>) => {
+				saveAlias(e, event.currentTarget.value, store);
+			};
+		},
+		[store, saveAlias],
+	);
+	return (
+		<ul>
+			{data &&
+				data.envVars &&
+				data.envVars.map((v) => {
+					if ("name" in v) {
+						const value = "alias" in v ? v.alias : v.name;
+						if (v.isEditting) {
+							return (
+								<li key={v.id}>
+									<Input
+										type="text"
+										className="uppercase"
+										defaultValue={value}
+										onKeyUp={saveAliasOnEnter(v)}
+										onBlur={saveAliasOnBlur(v)}
+										autoFocus={true}
+										disabled={disabled}
+									/>
+								</li>
+							);
+						}
+						return (
+							<li key={v.id} onClick={editAlias(v)}>
+								<TooltipProvider>
+									<Tooltip>
+										<TooltipTrigger className="w-full">
+											<div className="w-full flex flex-row items-center gap-1 cursor-text">
+												<Pencil className="w-4 h-4" />
+												<div className="uppercase">{value}</div>
+											</div>
+										</TooltipTrigger>
+										<TooltipContent>{v.name}</TooltipContent>
+									</Tooltip>
+								</TooltipProvider>
+							</li>
+						);
+					}
+				})}
+		</ul>
+	);
+}
+
+function Dev({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
+	const env = useEnv();
+	const store = useStateStore();
 	const devForm = useForm<z.infer<typeof devSchema>>({
 		resolver: zodResolver(devSchema),
 		mode: "onChange",
@@ -627,215 +801,6 @@
 	}, [id, data, exposeForm, store]);
 	return (
 		<>
-			<Form {...exposeForm}>
-				<form className="space-y-2">
-					<FormField
-						control={form.control}
-						name="name"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input
-										placeholder="name"
-										className="lowercase"
-										{...field}
-										ref={focus(field, "name")}
-										disabled={disabled}
-									/>
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<FormField
-						control={form.control}
-						name="type"
-						render={({ field }) => (
-							<FormItem>
-								<Select
-									onValueChange={field.onChange}
-									defaultValue={field.value}
-									{...typeProps}
-									disabled={disabled}
-								>
-									<FormControl>
-										<SelectTrigger>
-											<SelectValue placeholder="Runtime" />
-										</SelectTrigger>
-									</FormControl>
-									<SelectContent>
-										{ServiceTypes.map((t) => (
-											<SelectItem key={t} value={t}>
-												{t}
-											</SelectItem>
-										))}
-									</SelectContent>
-								</Select>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-				</form>
-			</Form>
-			Source
-			<Form {...sourceForm}>
-				<form className="space-y-2">
-					<FormField
-						control={sourceForm.control}
-						name="id"
-						render={({ field }) => (
-							<FormItem>
-								<Select onValueChange={field.onChange} defaultValue={field.value} disabled={disabled}>
-									<FormControl>
-										<SelectTrigger>
-											<SelectValue placeholder="Repository" />
-										</SelectTrigger>
-									</FormControl>
-									<SelectContent>
-										{(
-											nodes.filter(
-												(n) => n.type === "github" && n.data.repository?.id !== undefined,
-											) as GithubNode[]
-										).map((n) => (
-											<SelectItem
-												key={n.id}
-												value={n.id}
-											>{`${n.data.repository?.fullName}`}</SelectItem>
-										))}
-									</SelectContent>
-								</Select>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<FormField
-						control={sourceForm.control}
-						name="branch"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="master" className="lowercase" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<FormField
-						control={sourceForm.control}
-						name="rootDir"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="/" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-				</form>
-			</Form>
-			Ports
-			<ul>
-				{data &&
-					data.ports &&
-					data.ports.map((p) => (
-						<li key={p.id} className="flex flex-row items-center gap-1">
-							<Button
-								size={"icon"}
-								variant={"ghost"}
-								onClick={() => removePort(p.id)}
-								className="w-4 h-4"
-								disabled={disabled}
-							>
-								<XIcon />
-							</Button>
-							<div>
-								{p.name} - {p.value}
-							</div>
-						</li>
-					))}
-			</ul>
-			<Form {...portForm}>
-				<form className="flex flex-row space-x-1" onSubmit={portForm.handleSubmit(onSubmit)}>
-					<FormField
-						control={portForm.control}
-						name="name"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="name" className="lowercase" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<FormField
-						control={portForm.control}
-						name="value"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="value" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<Button type="submit" disabled={disabled}>
-						Add Port
-					</Button>
-				</form>
-			</Form>
-			Env Vars
-			<ul>
-				{data &&
-					data.envVars &&
-					data.envVars.map((v) => {
-						if ("name" in v) {
-							const value = "alias" in v ? v.alias : v.name;
-							if (v.isEditting) {
-								return (
-									<li key={v.id}>
-										<Input
-											type="text"
-											className="uppercase"
-											defaultValue={value}
-											onKeyUp={saveAliasOnEnter(v)}
-											onBlur={saveAliasOnBlur(v)}
-											autoFocus={true}
-											disabled={disabled}
-										/>
-									</li>
-								);
-							}
-							return (
-								<li key={v.id} onClick={editAlias(v)}>
-									<TooltipProvider>
-										<Tooltip>
-											<TooltipTrigger>
-												<div className="flex flex-row items-center gap-1">
-													<Button size={"icon"} variant={"ghost"} className="w-4 h-4">
-														<PencilIcon />
-													</Button>
-													<div>{value}</div>
-												</div>
-											</TooltipTrigger>
-											<TooltipContent>{v.name}</TooltipContent>
-										</Tooltip>
-									</TooltipProvider>
-								</li>
-							);
-						}
-					})}
-			</ul>
-			Pre-Build Commands
-			<Textarea
-				placeholder="new line separated list of commands to run before running the service"
-				value={data.preBuildCommands}
-				onChange={setPreBuildCommands}
-				disabled={disabled}
-			/>
-			Dev
 			<Form {...devForm}>
 				<form className="space-y-2">
 					<FormField
@@ -844,13 +809,13 @@
 						render={({ field }) => (
 							<FormItem>
 								<div className="flex flex-row gap-1 items-center">
-									<Checkbox
+									<Switch
 										id="devEnabled"
 										onCheckedChange={field.onChange}
 										checked={field.value}
 										disabled={disabled}
 									/>
-									<Label htmlFor="devEnabled">Enabled</Label>
+									<Label htmlFor="devEnabled">Dev VM</Label>
 								</div>
 								<FormMessage />
 							</FormItem>
@@ -861,6 +826,7 @@
 			{data.dev && data.dev.enabled && (
 				<Form {...exposeForm}>
 					<form className="space-y-2">
+						<Label>Network</Label>
 						<FormField
 							control={exposeForm.control}
 							name="network"
@@ -868,12 +834,12 @@
 								<FormItem>
 									<Select
 										onValueChange={field.onChange}
-										defaultValue={field.value}
+										value={field.value || ""}
 										disabled={disabled}
 									>
 										<FormControl>
 											<SelectTrigger>
-												<SelectValue placeholder="Network" />
+												<SelectValue />
 											</SelectTrigger>
 										</FormControl>
 										<SelectContent>
@@ -889,13 +855,14 @@
 								</FormItem>
 							)}
 						/>
+						<Label>Subdomain</Label>
 						<FormField
 							control={exposeForm.control}
 							name="subdomain"
 							render={({ field }) => (
 								<FormItem>
 									<FormControl>
-										<Input placeholder="subdomain" {...field} disabled={disabled} />
+										<Input {...field} disabled={disabled} />
 									</FormControl>
 									<FormMessage />
 								</FormItem>
@@ -907,3 +874,294 @@
 		</>
 	);
 }
+
+function SourceRepo({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
+	const { id, data } = node;
+	const store = useStateStore();
+	const nodes = useNodes<AppNode>();
+	const repo = useMemo(() => {
+		return nodes
+			.filter((n): n is GithubNode => n.type === "github")
+			.find((n) => n.id === data.repository?.repoNodeId);
+	}, [nodes, data.repository?.repoNodeId]);
+	const repos = useGithubRepositories();
+	const sourceForm = useForm<z.infer<typeof sourceSchema>>({
+		resolver: zodResolver(sourceSchema),
+		mode: "onChange",
+		defaultValues: {
+			id: data?.repository?.id?.toString(),
+			branch: data.repository && "branch" in data.repository ? data.repository.branch : undefined,
+			rootDir: data.repository && "rootDir" in data.repository ? data.repository.rootDir : undefined,
+		},
+	});
+	useEffect(() => {
+		const sub = sourceForm.watch(
+			(
+				value: DeepPartial<z.infer<typeof sourceSchema>>,
+				{ name }: { name?: keyof z.infer<typeof sourceSchema> | undefined; type?: EventType | undefined },
+			) => {
+				if (name === "id") {
+					const newRepoId = value.id ? parseInt(value.id, 10) : undefined;
+					if (!newRepoId) return;
+
+					const oldGithubNodeId = data.repository?.repoNodeId;
+					const selectedRepo = repos.find((r) => r.id === newRepoId);
+
+					if (!selectedRepo) return;
+
+					// If a node for the selected repo already exists, connect to it.
+					const existingNodeForSelectedRepo = nodes
+						.filter((n): n is GithubNode => n.type === "github")
+						.find((n) => n.data.repository?.id === selectedRepo.id);
+
+					if (existingNodeForSelectedRepo) {
+						let { nodes, edges } = store;
+						if (oldGithubNodeId) {
+							edges = edges.filter(
+								(e) =>
+									!(
+										e.target === id &&
+										e.source === oldGithubNodeId &&
+										e.targetHandle === "repository"
+									),
+							);
+						}
+						edges = edges.concat({
+							id: uuidv4(),
+							source: existingNodeForSelectedRepo.id,
+							sourceHandle: "repository",
+							target: id,
+							targetHandle: "repository",
+						});
+						nodes = nodes.map((n) => {
+							if (n.id !== id) {
+								return n;
+							} else {
+								const sn = n as ServiceNode;
+								return {
+									...sn,
+									data: {
+										...sn.data,
+										repository: {
+											...sn.data.repository,
+											id: newRepoId,
+											repoNodeId: existingNodeForSelectedRepo.id,
+										},
+									},
+								};
+							}
+						});
+						if (oldGithubNodeId && oldGithubNodeId !== existingNodeForSelectedRepo.id) {
+							const isOldNodeStillUsed = edges.some(
+								(e) => e.source === oldGithubNodeId && e.sourceHandle === "repository",
+							);
+							if (!isOldNodeStillUsed) {
+								nodes = nodes.filter((n) => n.id !== oldGithubNodeId);
+							}
+						}
+						store.setNodes(nodes);
+						store.setEdges(edges);
+						return;
+					}
+
+					// No node for selected repo, decide whether to update old node or create a new one.
+					if (oldGithubNodeId) {
+						const isOldNodeShared =
+							store.edges.filter(
+								(e) =>
+									e.source === oldGithubNodeId && e.target !== id && e.sourceHandle === "repository",
+							).length > 0;
+
+						if (!isOldNodeShared) {
+							// Update old node
+							store.updateNodeData<"github">(oldGithubNodeId, {
+								repository: {
+									id: selectedRepo.id,
+									sshURL: selectedRepo.ssh_url,
+									fullName: selectedRepo.full_name,
+								},
+								label: selectedRepo.full_name,
+							});
+							store.updateNodeData<"app">(id, {
+								repository: {
+									...data.repository,
+									id: newRepoId,
+								},
+							});
+						} else {
+							// Create new node because old one is shared
+							const newGithubNodeId = uuidv4();
+							store.addNode({
+								id: newGithubNodeId,
+								type: "github",
+								data: {
+									repository: {
+										id: selectedRepo.id,
+										sshURL: selectedRepo.ssh_url,
+										fullName: selectedRepo.full_name,
+									},
+									label: selectedRepo.full_name,
+									envVars: [],
+									ports: [],
+								},
+							});
+
+							let edges = store.edges;
+							// remove old edge
+							edges = edges.filter(
+								(e) =>
+									!(
+										e.target === id &&
+										e.source === oldGithubNodeId &&
+										e.targetHandle === "repository"
+									),
+							);
+							// add new edge
+							edges = edges.concat({
+								id: uuidv4(),
+								source: newGithubNodeId,
+								sourceHandle: "repository",
+								target: id,
+								targetHandle: "repository",
+							});
+							store.setEdges(edges);
+							store.updateNodeData<"app">(id, {
+								repository: {
+									...data.repository,
+									id: newRepoId,
+									repoNodeId: newGithubNodeId,
+								},
+							});
+						}
+					} else {
+						// No old github node, so create a new one
+						const newGithubNodeId = uuidv4();
+						store.addNode({
+							id: newGithubNodeId,
+							type: "github",
+							data: {
+								repository: {
+									id: selectedRepo.id,
+									sshURL: selectedRepo.ssh_url,
+									fullName: selectedRepo.full_name,
+								},
+								label: selectedRepo.full_name,
+								envVars: [],
+								ports: [],
+							},
+						});
+						store.setEdges(
+							store.edges.concat({
+								id: uuidv4(),
+								source: newGithubNodeId,
+								sourceHandle: "repository",
+								target: id,
+								targetHandle: "repository",
+							}),
+						);
+						store.updateNodeData<"app">(id, {
+							repository: {
+								...data.repository,
+								id: newRepoId,
+								repoNodeId: newGithubNodeId,
+							},
+						});
+					}
+				} else if (name === "branch") {
+					store.updateNodeData<"app">(id, {
+						repository: {
+							...data?.repository,
+							branch: value.branch,
+						},
+					});
+				} else if (name === "rootDir") {
+					store.updateNodeData<"app">(id, {
+						repository: {
+							...data?.repository,
+							rootDir: value.rootDir,
+						},
+					});
+				}
+			},
+		);
+		return () => sub.unsubscribe();
+	}, [id, data, sourceForm, store, nodes, repos]);
+	const [isExpanded, setIsExpanded] = useState(false);
+	// useEffect(() => {
+	// 	if (data.repository === undefined) {
+	// 		setIsExpanded(true);
+	// 	}
+	// }, [data.repository, setIsExpanded]);
+	console.log(data.repository, isExpanded, repo);
+	return (
+		<Accordion type="single" collapsible>
+			<AccordionItem value="repository" className="border-none">
+				<AccordionTrigger onClick={() => setIsExpanded(!isExpanded)}>
+					Source {!isExpanded && repo !== undefined && repo.data.repository?.fullName}
+				</AccordionTrigger>
+				<AccordionContent className="px-1">
+					<Form {...sourceForm}>
+						<form className="space-y-2">
+							<Label>Repository</Label>
+							<FormField
+								control={sourceForm.control}
+								name="id"
+								render={({ field }) => (
+									<FormItem>
+										<Select onValueChange={field.onChange} value={field.value} disabled={disabled}>
+											<FormControl>
+												<SelectTrigger>
+													<SelectValue />
+												</SelectTrigger>
+											</FormControl>
+											<SelectContent>
+												{repos.map((r) => (
+													<SelectItem
+														key={r.id}
+														value={r.id.toString()}
+													>{`${r.full_name}`}</SelectItem>
+												))}
+											</SelectContent>
+										</Select>
+										<FormMessage />
+									</FormItem>
+								)}
+							/>
+							<Label>Branch</Label>
+							<FormField
+								control={sourceForm.control}
+								name="branch"
+								render={({ field }) => (
+									<FormItem>
+										<FormControl>
+											<Input
+												placeholder="master"
+												className="lowercase"
+												{...field}
+												disabled={disabled}
+											/>
+										</FormControl>
+										<FormMessage />
+									</FormItem>
+								)}
+							/>
+							<Label>Root Directory</Label>
+							<FormField
+								control={sourceForm.control}
+								name="rootDir"
+								render={({ field }) => (
+									<FormItem>
+										<FormControl>
+											<Input placeholder="/" {...field} disabled={disabled} />
+										</FormControl>
+										<FormMessage />
+									</FormItem>
+								)}
+							/>
+						</form>
+					</Form>
+				</AccordionContent>
+			</AccordionItem>
+		</Accordion>
+	);
+}
diff --git a/apps/canvas/front/src/components/node-details.tsx b/apps/canvas/front/src/components/node-details.tsx
index 2d9cd58..d120c76 100644
--- a/apps/canvas/front/src/components/node-details.tsx
+++ b/apps/canvas/front/src/components/node-details.tsx
@@ -22,7 +22,7 @@
 function NodeDetailsImpl(props: NodeDetailsProps) {
 	switch (props.type) {
 		case "app":
-			return <NodeAppDetails {...props} />;
+			return <NodeAppDetails node={props} disabled={props.disabled} />;
 		case "gateway-https":
 			return <NodeGatewayHttpsDetails {...props} />;
 		case "gateway-tcp":
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index 81c40c3..1eb0409 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -17,10 +17,10 @@
 import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
 import { Input } from "./ui/input";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
-import { Checkbox } from "./ui/checkbox";
 import { Label } from "./ui/label";
 import { Button } from "./ui/button";
 import { XIcon } from "lucide-react";
+import { Switch } from "./ui/switch";
 
 const schema = z.object({
 	network: z.string().min(1, "reqired"),
@@ -411,7 +411,7 @@
 								render={({ field }) => (
 									<FormItem>
 										<div className="flex flex-row gap-1 items-center">
-											<Checkbox
+											<Switch
 												id="authEnabled"
 												onCheckedChange={field.onChange}
 												checked={field.value}
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index dd7c68a..4373359 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -20,7 +20,6 @@
 import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
 import { Handle, Position } from "@xyflow/react";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
-import { GitHubRepository } from "../lib/github";
 import { useProjectId } from "@/lib/state";
 import { Alert, AlertDescription } from "./ui/alert";
 import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
@@ -64,36 +63,11 @@
 	const repoError = useGithubRepositoriesError();
 	const fetchStoreRepositories = useFetchGithubRepositories();
 
-	const [displayRepos, setDisplayRepos] = useState<GitHubRepository[]>([]);
-
 	const [isAnalyzing, setIsAnalyzing] = useState(false);
 	const [showModal, setShowModal] = useState(false);
 	const [discoveredServices, setDiscoveredServices] = useState<z.infer<typeof serviceAnalyzisSchema>[]>([]);
 	const [selectedServices, setSelectedServices] = useState<Record<string, boolean>>({});
 
-	useEffect(() => {
-		let currentRepoInStore = false;
-		if (data.repository) {
-			currentRepoInStore = storeRepos.some((r) => r.id === data.repository!.id);
-		}
-
-		if (data.repository && !currentRepoInStore) {
-			const currentRepoForDisplay: GitHubRepository = {
-				id: data.repository.id,
-				name: data.repository.sshURL.split("/").pop() || "",
-				full_name: data.repository.fullName || data.repository.sshURL.split("/").slice(-2).join("/"),
-				html_url: "",
-				ssh_url: data.repository.sshURL,
-				description: null,
-				private: false,
-				default_branch: "main",
-			};
-			setDisplayRepos([currentRepoForDisplay, ...storeRepos.filter((r) => r.id !== data.repository!.id)]);
-		} else {
-			setDisplayRepos(storeRepos);
-		}
-	}, [data.repository, storeRepos]);
-
 	const form = useForm<z.infer<typeof schema>>({
 		resolver: zodResolver(schema),
 		mode: "onChange",
@@ -118,7 +92,7 @@
 				switch (name) {
 					case "repositoryId":
 						if (value.repositoryId) {
-							const repo = displayRepos.find((r) => r.id === value.repositoryId);
+							const repo = storeRepos.find((r) => r.id === value.repositoryId);
 							if (repo) {
 								store.updateNodeData<"github">(id, {
 									repository: {
@@ -134,7 +108,7 @@
 			},
 		);
 		return () => sub.unsubscribe();
-	}, [form, store, id, displayRepos]);
+	}, [form, store, id, storeRepos]);
 
 	const analyze = useCallback(async () => {
 		if (!data.repository?.sshURL) return;
@@ -177,7 +151,8 @@
 				const newNodeData: Omit<ServiceData, "activeField" | "state"> = {
 					label: service.name,
 					repository: {
-						id: id,
+						id: data.repository!.id,
+						repoNodeId: id,
 					},
 					info: service,
 					type: "nodejs:24.0.2" as ServiceType,
@@ -239,7 +214,7 @@
 															githubService
 																? isLoadingRepos
 																	? "Loading..."
-																	: displayRepos.length === 0
+																	: storeRepos.length === 0
 																		? "No repositories found"
 																		: "Select a repository"
 																: "GitHub not configured"
@@ -248,7 +223,7 @@
 												</SelectTrigger>
 											</FormControl>
 											<SelectContent>
-												{displayRepos.map((repo) => (
+												{storeRepos.map((repo) => (
 													<SelectItem key={repo.id} value={repo.id.toString()}>
 														{repo.full_name}
 														{repo.description && ` - ${repo.description}`}
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 5d8013d..a0e4e91 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -132,16 +132,19 @@
 
 export type ServiceData = NodeData & {
 	type: ServiceType;
-	repository:
+	repository?:
 		| {
-				id: string;
+				id: number;
+				repoNodeId: string;
 		  }
 		| {
-				id: string;
+				id: number;
+				repoNodeId: string;
 				branch: string;
 		  }
 		| {
-				id: string;
+				id: number;
+				repoNodeId: string;
 				branch: string;
 				rootDir: string;
 		  };
@@ -640,7 +643,10 @@
 	return ret;
 }
 
-export const useStateStore = create<AppState>((set, get): AppState => {
+export const useStateStore = create<AppState>((setOg, get): AppState => {
+	const set = (state: Partial<AppState>) => {
+		setOg(state);
+	};
 	const setN = (nodes: AppNode[]) => {
 		set({
 			nodes,
@@ -845,6 +851,17 @@
 					});
 				}
 			}
+			if (c.targetHandle === "repository") {
+				const sourceNode = nodes.find((n) => n.id === c.source);
+				if (sourceNode && sourceNode.type === "github" && sourceNode.data.repository) {
+					updateNodeData<"app">(tn.id, {
+						repository: {
+							id: sourceNode.data.repository.id,
+							repoNodeId: c.source,
+						},
+					});
+				}
+			}
 		}
 		if (c.sourceHandle === "volume") {
 			updateNodeData<"volume">(c.source, {
@@ -900,17 +917,6 @@
 				});
 			}
 		}
-		if (tn.type === "app") {
-			if (c.targetHandle === "repository") {
-				updateNodeData<"app">(tn.id, {
-					repository: {
-						id: c.source,
-						branch: "master",
-						rootDir: "/",
-					},
-				});
-			}
-		}
 	}
 
 	const fetchGithubRepositories = async () => {