Canvas: Import modal

Change-Id: I22928007c5b81d93be2eed2d133fed4d73e1703f
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index daeded2..a2e8b31 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -772,38 +772,50 @@
 		resp.status(400).send({ error: "GitHub token not configured" });
 		return;
 	}
-	let deployKey: string | null = project.deployKey;
-	let deployKeyPublic: string | null = project.deployKeyPublic;
-	if (!deployKeyPublic) {
-		[deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
-		await db.project.update({
-			where: { id: projectId },
-			data: {
-				deployKeyPublic: deployKeyPublic,
-				deployKey: deployKey,
-			},
+	let tmpDir: tmp.DirResult | null = null;
+	try {
+		let deployKey: string | null = project.deployKey;
+		let deployKeyPublic: string | null = project.deployKeyPublic;
+		if (!deployKeyPublic) {
+			[deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
+			await db.project.update({
+				where: { id: projectId },
+				data: {
+					deployKeyPublic: deployKeyPublic,
+					deployKey: deployKey,
+				},
+			});
+		}
+		const github = new GithubClient(project.githubToken);
+		const result = analyzeRepoReqSchema.safeParse(req.body);
+		if (!result.success) {
+			resp.status(400).send({ error: "Invalid request data" });
+			return;
+		}
+		const { address } = result.data;
+		tmpDir = tmp.dirSync({
+			unsafeCleanup: true,
 		});
+		await github.addDeployKey(address, deployKeyPublic);
+		await fs.promises.writeFile(path.join(tmpDir.name, "key"), deployKey!, {
+			mode: 0o600,
+		});
+		shell.exec(
+			`GIT_SSH_COMMAND='ssh -i ${tmpDir.name}/key -o IdentitiesOnly=yes' git clone ${address} ${tmpDir.name}/code`,
+		);
+		const fsc = new RealFileSystem(`${tmpDir.name}/code`);
+		const analyzer = new NodeJSAnalyzer();
+		const info = await analyzer.analyze(fsc, "/");
+		resp.status(200).send([info]);
+	} catch (e) {
+		console.error(e);
+		resp.status(500).send({ error: "Failed to analyze repository" });
+	} finally {
+		if (tmpDir) {
+			tmpDir.removeCallback();
+		}
+		resp.end();
 	}
-	const github = new GithubClient(project.githubToken);
-	const result = analyzeRepoReqSchema.safeParse(req.body);
-	if (!result.success) {
-		resp.status(400).send({ error: "Invalid request data" });
-		return;
-	}
-	const { address } = result.data;
-	const tmpDir = tmp.dirSync();
-	await github.addDeployKey(address, deployKeyPublic);
-	await fs.promises.writeFile(path.join(tmpDir.name, "key"), deployKey!, {
-		mode: 0o600,
-	});
-	shell.exec(
-		`GIT_SSH_COMMAND='ssh -i ${tmpDir.name}/key -o IdentitiesOnly=yes' git clone ${address} ${tmpDir.name}/code`,
-	);
-	const fsc = new RealFileSystem(`${tmpDir.name}/code`);
-	const analyzer = new NodeJSAnalyzer();
-	const info = await analyzer.analyze(fsc, "/");
-	console.log(info);
-	resp.status(200).send([info]);
 };
 
 const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index d87f458..d4bd5ea 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -12,6 +12,7 @@
 	DropdownMenuTrigger,
 } from "./ui/dropdown-menu";
 import { Ellipsis, LoaderCircle } from "lucide-react";
+import { ImportModal } from "./import-modal";
 
 function toNodeType(t: string): string {
 	if (t === "ingress") {
@@ -34,6 +35,7 @@
 	const [ok, setOk] = useState(false);
 	const [loading, setLoading] = useState(false);
 	const [reloading, setReloading] = useState(false);
+	const [showImportModal, setShowImportModal] = useState(false);
 	const info = useCallback(
 		(title: string, description?: string, duration?: number) => {
 			return toast({
@@ -309,47 +311,51 @@
 		);
 	} else {
 		return (
-			<div className="flex flex-row gap-1 items-center">
-				<Button onClick={deploy} {...deployProps}>
-					{deployProps.loading ? (
-						<>
-							<LoaderCircle className="animate-spin" />
-							Deploying...
-						</>
-					) : (
-						"Deploy"
-					)}
-				</Button>
-				<Button onClick={save}>Save</Button>
-				<DropdownMenu>
-					<DropdownMenuTrigger>
-						<Button size="icon">
-							<Ellipsis />
-						</Button>
-					</DropdownMenuTrigger>
-					<DropdownMenuContent className="w-56">
-						<DropdownMenuGroup>
-							<DropdownMenuItem
-								onClick={restoreSaved}
-								disabled={projectId === undefined}
-								className="cursor-pointer hover:bg-gray-200"
-							>
-								Restore
-							</DropdownMenuItem>
-							<DropdownMenuItem onClick={clear} className="cursor-pointer hover:bg-gray-200">
-								Clear
-							</DropdownMenuItem>
-							<DropdownMenuItem
-								onClick={deleteProject}
-								disabled={projectId === undefined}
-								className="cursor-pointer hover:bg-gray-200"
-							>
-								Delete project
-							</DropdownMenuItem>
-						</DropdownMenuGroup>
-					</DropdownMenuContent>
-				</DropdownMenu>
-			</div>
+			<>
+				<div className="flex flex-row gap-1 items-center">
+					<Button onClick={deploy} {...deployProps}>
+						{deployProps.loading ? (
+							<>
+								<LoaderCircle className="animate-spin" />
+								Deploying...
+							</>
+						) : (
+							"Deploy"
+						)}
+					</Button>
+					<Button onClick={save}>Save</Button>
+					<Button onClick={() => setShowImportModal(true)}>Import</Button>
+					<DropdownMenu>
+						<DropdownMenuTrigger>
+							<Button size="icon">
+								<Ellipsis />
+							</Button>
+						</DropdownMenuTrigger>
+						<DropdownMenuContent className="w-56">
+							<DropdownMenuGroup>
+								<DropdownMenuItem
+									onClick={restoreSaved}
+									disabled={projectId === undefined}
+									className="cursor-pointer hover:bg-gray-200"
+								>
+									Restore
+								</DropdownMenuItem>
+								<DropdownMenuItem onClick={clear} className="cursor-pointer hover:bg-gray-200">
+									Clear
+								</DropdownMenuItem>
+								<DropdownMenuItem
+									onClick={deleteProject}
+									disabled={projectId === undefined}
+									className="cursor-pointer hover:bg-gray-200"
+								>
+									Delete project
+								</DropdownMenuItem>
+							</DropdownMenuGroup>
+						</DropdownMenuContent>
+					</DropdownMenu>
+				</div>
+				<ImportModal open={showImportModal} onOpenChange={setShowImportModal} />
+			</>
 		);
 	}
 }
diff --git a/apps/canvas/front/src/components/import-modal.tsx b/apps/canvas/front/src/components/import-modal.tsx
new file mode 100644
index 0000000..ea9a06c
--- /dev/null
+++ b/apps/canvas/front/src/components/import-modal.tsx
@@ -0,0 +1,352 @@
+import { useCallback, useEffect, useState } from "react";
+import { z } from "zod";
+import { useForm, useWatch } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
+import {
+	useProjectId,
+	useGithubService,
+	useGithubRepositories,
+	useGithubRepositoriesLoading,
+	useGithubRepositoriesError,
+	useFetchGithubRepositories,
+	serviceAnalyzisSchema,
+	ServiceType,
+	ServiceData,
+	useStateStore,
+} from "@/lib/state";
+import { Alert, AlertDescription } from "./ui/alert";
+import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
+import { Button } from "./ui/button";
+import { v4 as uuidv4 } from "uuid";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
+import { Switch } from "./ui/switch";
+import { Label } from "./ui/label";
+import { useToast } from "@/hooks/use-toast";
+
+const schema = z.object({
+	repositoryId: z.number().optional(),
+});
+
+interface ImportModalProps {
+	open: boolean;
+	onOpenChange: (open: boolean) => void;
+	initialRepositoryId?: number;
+}
+
+export function ImportModal({ open, onOpenChange, initialRepositoryId }: ImportModalProps) {
+	const { toast } = useToast();
+	const store = useStateStore();
+	const projectId = useProjectId();
+	const githubService = useGithubService();
+	const storeRepos = useGithubRepositories();
+	const isLoadingRepos = useGithubRepositoriesLoading();
+	const repoError = useGithubRepositoriesError();
+	const fetchStoreRepositories = useFetchGithubRepositories();
+
+	const [isAnalyzing, setIsAnalyzing] = useState(false);
+	const [analysisAttempted, setAnalysisAttempted] = useState(false);
+	const [discoveredServices, setDiscoveredServices] = useState<z.infer<typeof serviceAnalyzisSchema>[]>([]);
+	const [selectedServices, setSelectedServices] = useState<Record<string, boolean>>({});
+
+	const form = useForm<z.infer<typeof schema>>({
+		resolver: zodResolver(schema),
+		mode: "onChange",
+		defaultValues: {
+			repositoryId: initialRepositoryId,
+		},
+	});
+
+	const selectedRepoId = useWatch({ control: form.control, name: "repositoryId" });
+
+	useEffect(() => {
+		form.reset({ repositoryId: initialRepositoryId });
+		setAnalysisAttempted(false);
+	}, [initialRepositoryId, form]);
+
+	// Clear analysis results when repository changes
+	useEffect(() => {
+		setDiscoveredServices([]);
+		setSelectedServices({});
+		setAnalysisAttempted(false);
+	}, [selectedRepoId]);
+
+	const analyze = useCallback(
+		async (sshURL: string) => {
+			if (!sshURL) return;
+
+			setIsAnalyzing(true);
+			try {
+				const resp = await fetch(`/api/project/${projectId}/analyze`, {
+					method: "POST",
+					body: JSON.stringify({
+						address: sshURL,
+					}),
+					headers: {
+						"Content-Type": "application/json",
+					},
+				});
+				const servicesResult = z.array(serviceAnalyzisSchema).safeParse(await resp.json());
+				if (!servicesResult.success) {
+					console.error(servicesResult.error);
+					toast({
+						variant: "destructive",
+						title: "Failed to analyze repository",
+					});
+					setIsAnalyzing(false);
+					return;
+				}
+
+				setDiscoveredServices(servicesResult.data);
+				const initialSelectedServices: Record<string, boolean> = {};
+				servicesResult.data.forEach((service) => {
+					initialSelectedServices[service.name] = true;
+				});
+				setSelectedServices(initialSelectedServices);
+			} catch (err) {
+				console.error("Analysis failed:", err);
+				toast({
+					variant: "destructive",
+					title: "Failed to analyze repository",
+				});
+			} finally {
+				setIsAnalyzing(false);
+			}
+		},
+		[projectId, toast],
+	);
+
+	// Auto-analyze when opened with initialRepositoryId
+	useEffect(() => {
+		if (open && initialRepositoryId && !isAnalyzing && !discoveredServices.length && !analysisAttempted) {
+			const repo = storeRepos.find((r) => r.id === initialRepositoryId);
+			if (repo?.ssh_url) {
+				setAnalysisAttempted(true);
+				analyze(repo.ssh_url);
+			}
+		}
+	}, [open, initialRepositoryId, isAnalyzing, discoveredServices.length, storeRepos, analyze, analysisAttempted]);
+
+	const handleImportServices = () => {
+		const repoId = form.getValues("repositoryId");
+		if (!repoId) return;
+
+		const repo = storeRepos.find((r) => r.id === repoId);
+		if (!repo) return;
+
+		// Check for existing GitHub node for this repository
+		const existingGithubNode = store.nodes.find((n) => n.type === "github" && n.data.repository?.id === repo.id);
+
+		const githubNodeId = existingGithubNode?.id || uuidv4();
+
+		// Only create a new GitHub node if one doesn't exist
+		if (!existingGithubNode) {
+			store.addNode({
+				id: githubNodeId,
+				type: "github",
+				data: {
+					label: repo.full_name,
+					repository: {
+						id: repo.id,
+						sshURL: repo.ssh_url,
+						fullName: repo.full_name,
+					},
+					envVars: [],
+					ports: [],
+					state: null,
+				},
+			});
+		}
+
+		discoveredServices.forEach((service) => {
+			if (selectedServices[service.name]) {
+				const newNodeData: Omit<ServiceData, "activeField" | "state"> = {
+					label: service.name,
+					repository: {
+						id: repo.id,
+						repoNodeId: githubNodeId,
+					},
+					info: service,
+					type: "nodejs:24.0.2" as ServiceType,
+					env: [],
+					volume: [],
+					preBuildCommands: "",
+					isChoosingPortToConnect: false,
+					envVars: [],
+					ports: [],
+				};
+				const newNodeId = uuidv4();
+				store.addNode({
+					id: newNodeId,
+					type: "app",
+					data: newNodeData,
+				});
+				let edges = store.edges;
+				edges = edges.concat({
+					id: uuidv4(),
+					source: githubNodeId,
+					sourceHandle: "repository",
+					target: newNodeId,
+					targetHandle: "repository",
+				});
+				store.setEdges(edges);
+			}
+		});
+
+		onOpenChange(false);
+		setDiscoveredServices([]);
+		setSelectedServices({});
+		form.reset();
+	};
+
+	return (
+		<Dialog open={open} onOpenChange={onOpenChange}>
+			<DialogContent className="sm:max-w-[425px]">
+				<DialogHeader>
+					<DialogTitle>Import Services</DialogTitle>
+					<DialogDescription>Select a repository and analyze it for services.</DialogDescription>
+				</DialogHeader>
+				<div className="grid gap-4 py-4">
+					<Form {...form}>
+						<form className="space-y-2">
+							<FormField
+								control={form.control}
+								name="repositoryId"
+								render={({ field }) => (
+									<FormItem>
+										<div className="flex items-center gap-2 w-full">
+											<div className="flex-grow">
+												<Select
+													onValueChange={(value) => field.onChange(Number(value))}
+													value={field.value?.toString()}
+													disabled={isLoadingRepos || !projectId || !githubService}
+												>
+													<FormControl>
+														<SelectTrigger>
+															<SelectValue
+																placeholder={
+																	githubService
+																		? isLoadingRepos
+																			? "Loading..."
+																			: storeRepos.length === 0
+																				? "No repositories found"
+																				: "Select a repository"
+																		: "GitHub not configured"
+																}
+															/>
+														</SelectTrigger>
+													</FormControl>
+													<SelectContent>
+														{storeRepos.map((repo) => (
+															<SelectItem
+																key={repo.id}
+																value={repo.id.toString()}
+																className="cursor-pointer hover:bg-gray-100"
+															>
+																{repo.full_name}
+																{repo.description && ` - ${repo.description}`}
+															</SelectItem>
+														))}
+													</SelectContent>
+												</Select>
+											</div>
+											{isLoadingRepos && (
+												<Button variant="ghost" size="icon" disabled>
+													<LoaderCircle className="h-5 w-5 animate-spin text-muted-foreground" />
+												</Button>
+											)}
+											{!isLoadingRepos && githubService && (
+												<Button
+													variant="ghost"
+													size="icon"
+													onClick={fetchStoreRepositories}
+													aria-label="Refresh repositories"
+												>
+													<RefreshCw className="h-5 w-5 text-muted-foreground" />
+												</Button>
+											)}
+										</div>
+										<FormMessage />
+										{repoError && <p className="text-sm text-red-500 mt-1">{repoError}</p>}
+										{!githubService && (
+											<Alert variant="destructive" className="mt-2">
+												<AlertCircle className="h-4 w-4" />
+												<AlertDescription>
+													Please configure Github Personal Access Token in the Integrations
+													tab.
+												</AlertDescription>
+											</Alert>
+										)}
+									</FormItem>
+								)}
+							/>
+						</form>
+					</Form>
+					<Button
+						disabled={!form.getValues("repositoryId") || isAnalyzing || !githubService}
+						onClick={() => {
+							const repo = storeRepos.find((r) => r.id === form.getValues("repositoryId"));
+							if (repo?.ssh_url) {
+								analyze(repo.ssh_url);
+							}
+						}}
+					>
+						{isAnalyzing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
+						{isAnalyzing ? "Analyzing..." : "Scan for services"}
+					</Button>
+					{discoveredServices.length > 0 && (
+						<div className="grid gap-4">
+							<h4 className="font-medium">Discovered Services</h4>
+							{discoveredServices.map((service) => (
+								<div key={service.name} className="flex flex-col space-y-2 p-2 border rounded-md">
+									<div className="flex items-center space-x-2">
+										<Switch
+											id={service.name}
+											checked={selectedServices[service.name]}
+											onCheckedChange={(checked: boolean) =>
+												setSelectedServices((prev) => ({
+													...prev,
+													[service.name]: checked,
+												}))
+											}
+										/>
+										<Label htmlFor={service.name} className="font-semibold">
+											{service.name}
+										</Label>
+									</div>
+									<div className="pl-6 text-sm text-gray-600">
+										<p>
+											<span className="font-medium">Location:</span> {service.location}
+										</p>
+										{service.configVars && service.configVars.length > 0 && (
+											<div className="mt-1">
+												<p className="font-medium">Environment Variables:</p>
+												<ul className="list-disc list-inside pl-4">
+													{service.configVars.map((envVar) => (
+														<li key={envVar.name}>{envVar.name}</li>
+													))}
+												</ul>
+											</div>
+										)}
+									</div>
+								</div>
+							))}
+						</div>
+					)}
+				</div>
+				<DialogFooter>
+					<Button variant="outline" onClick={() => onOpenChange(false)}>
+						Cancel
+					</Button>
+					<Button
+						onClick={handleImportServices}
+						disabled={!discoveredServices.length || !Object.values(selectedServices).some(Boolean)}
+					>
+						Import Selected Services
+					</Button>
+				</DialogFooter>
+			</DialogContent>
+		</Dialog>
+	);
+}
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index b75f7b1..ef289cb 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -3,17 +3,15 @@
 	GithubNode,
 	nodeIsConnectable,
 	nodeLabel,
-	serviceAnalyzisSchema,
 	useStateStore,
 	useGithubService,
-	ServiceType,
-	ServiceData,
 	useGithubRepositories,
 	useGithubRepositoriesLoading,
 	useGithubRepositoriesError,
 	useFetchGithubRepositories,
+	AppNode,
 } from "@/lib/state";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
 import { z } from "zod";
 import { DeepPartial, EventType, useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
@@ -24,11 +22,8 @@
 import { Alert, AlertDescription } from "./ui/alert";
 import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
 import { Button } from "./ui/button";
-import { v4 as uuidv4 } from "uuid";
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
-import { Switch } from "./ui/switch";
-import { Label } from "./ui/label";
 import { NodeDetailsProps } from "@/lib/types";
+import { ImportModal } from "./import-modal";
 
 export function NodeGithub(node: GithubNode) {
 	const { id, selected } = node;
@@ -65,10 +60,7 @@
 	const repoError = useGithubRepositoriesError();
 	const fetchStoreRepositories = useFetchGithubRepositories();
 
-	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>>({});
+	const [showImportModal, setShowImportModal] = useState(false);
 
 	const form = useForm<z.infer<typeof schema>>({
 		resolver: zodResolver(schema),
@@ -95,15 +87,33 @@
 					case "repositoryId":
 						if (value.repositoryId) {
 							const repo = storeRepos.find((r) => r.id === value.repositoryId);
-							if (repo) {
-								store.updateNodeData<"github">(id, {
-									repository: {
-										id: repo.id,
-										sshURL: repo.ssh_url,
-										fullName: repo.full_name,
-									},
-								});
+							if (!repo) {
+								return;
 							}
+							store.setNodes(
+								store.nodes.map((n): AppNode => {
+									if (n.type === "github" && n.id === id) {
+										return {
+											...n,
+											data: {
+												...n.data,
+												repository: {
+													id: repo.id,
+													sshURL: repo.ssh_url,
+													fullName: repo.full_name,
+												},
+											},
+										};
+									} else if (n.type === "app" && n.data.repository?.repoNodeId === id) {
+										return {
+											...n,
+											data: { ...n.data, repository: { id: repo.id, repoNodeId: id } },
+										};
+									} else {
+										return n;
+									}
+								}),
+							);
 						}
 						break;
 				}
@@ -112,87 +122,6 @@
 		return () => sub.unsubscribe();
 	}, [form, store, id, storeRepos]);
 
-	const analyze = useCallback(async () => {
-		if (!data.repository?.sshURL) return;
-
-		setIsAnalyzing(true);
-		try {
-			const resp = await fetch(`/api/project/${projectId}/analyze`, {
-				method: "POST",
-				body: JSON.stringify({
-					address: data.repository?.sshURL,
-				}),
-				headers: {
-					"Content-Type": "application/json",
-				},
-			});
-			const servicesResult = z.array(serviceAnalyzisSchema).safeParse(await resp.json());
-			if (!servicesResult.success) {
-				console.error(servicesResult.error);
-				setIsAnalyzing(false);
-				return;
-			}
-
-			setDiscoveredServices(servicesResult.data);
-			const initialSelectedServices: Record<string, boolean> = {};
-			servicesResult.data.forEach((service) => {
-				initialSelectedServices[service.name] = true;
-			});
-			setSelectedServices(initialSelectedServices);
-			setShowModal(true);
-		} catch (err) {
-			console.error("Analysis failed:", err);
-		} finally {
-			setIsAnalyzing(false);
-		}
-	}, [projectId, data.repository?.sshURL]);
-
-	const handleImportServices = () => {
-		discoveredServices.forEach((service) => {
-			if (selectedServices[service.name]) {
-				const newNodeData: Omit<ServiceData, "activeField" | "state"> = {
-					label: service.name,
-					repository: {
-						id: data.repository!.id,
-						repoNodeId: id,
-					},
-					info: service,
-					type: "nodejs:24.0.2" as ServiceType,
-					env: [],
-					volume: [],
-					preBuildCommands: "",
-					isChoosingPortToConnect: false,
-					envVars: [],
-					ports: [],
-				};
-				const newNodeId = uuidv4();
-				store.addNode({
-					id: newNodeId,
-					type: "app",
-					data: newNodeData,
-				});
-				let edges = store.edges;
-				edges = edges.concat({
-					id: uuidv4(),
-					source: id,
-					sourceHandle: "repository",
-					target: newNodeId,
-					targetHandle: "repository",
-				});
-				store.setEdges(edges);
-			}
-		});
-		setShowModal(false);
-		setDiscoveredServices([]);
-		setSelectedServices({});
-	};
-
-	const handleCancelModal = () => {
-		setShowModal(false);
-		setDiscoveredServices([]);
-		setSelectedServices({});
-	};
-
 	return (
 		<>
 			<Form {...form}>
@@ -266,62 +195,17 @@
 					/>
 				</form>
 			</Form>
-			<Button disabled={!data.repository?.sshURL || isAnalyzing || !githubService || disabled} onClick={analyze}>
-				{isAnalyzing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
+			<Button
+				disabled={!data.repository?.sshURL || !githubService || disabled}
+				onClick={() => setShowImportModal(true)}
+			>
 				Scan for services
 			</Button>
-			{showModal && (
-				<Dialog open={showModal} onOpenChange={setShowModal}>
-					<DialogContent className="sm:max-w-[425px]">
-						<DialogHeader>
-							<DialogTitle>Discovered Services</DialogTitle>
-							<DialogDescription>Select the services you want to import.</DialogDescription>
-						</DialogHeader>
-						<div className="grid gap-4 py-4">
-							{discoveredServices.map((service) => (
-								<div key={service.name} className="flex flex-col space-y-2 p-2 border rounded-md">
-									<div className="flex items-center space-x-2">
-										<Switch
-											id={service.name}
-											checked={selectedServices[service.name]}
-											onCheckedChange={(checked: boolean) =>
-												setSelectedServices((prev) => ({
-													...prev,
-													[service.name]: checked,
-												}))
-											}
-										/>
-										<Label htmlFor={service.name} className="font-semibold">
-											{service.name}
-										</Label>
-									</div>
-									<div className="pl-6 text-sm text-gray-600">
-										<p>
-											<span className="font-medium">Location:</span> {service.location}
-										</p>
-										{service.configVars && service.configVars.length > 0 && (
-											<div className="mt-1">
-												<p className="font-medium">Environment Variables:</p>
-												<ul className="list-disc list-inside pl-4">
-													{service.configVars.map((envVar) => (
-														<li key={envVar.name}>{envVar.name}</li>
-													))}
-												</ul>
-											</div>
-										)}
-									</div>
-								</div>
-							))}
-						</div>
-						<DialogFooter>
-							<Button variant="outline" onClick={handleCancelModal}>
-								Cancel
-							</Button>
-							<Button onClick={handleImportServices}>Import</Button>
-						</DialogFooter>
-					</DialogContent>
-				</Dialog>
-			)}
+			<ImportModal
+				open={showImportModal}
+				onOpenChange={setShowImportModal}
+				initialRepositoryId={data.repository?.id}
+			/>
 		</>
 	);
 }
diff --git a/apps/canvas/front/src/components/node-name.tsx b/apps/canvas/front/src/components/node-name.tsx
index 7a68f83..4a62206 100644
--- a/apps/canvas/front/src/components/node-name.tsx
+++ b/apps/canvas/front/src/components/node-name.tsx
@@ -1,5 +1,5 @@
 import { useState, useEffect } from "react";
-import { useStateStore } from "@/lib/state";
+import { nodeLabel, useStateStore } from "@/lib/state";
 import { AppNode } from "@/lib/state";
 import { Icon } from "./icon";
 import { Input } from "./ui/input";
@@ -21,6 +21,16 @@
 			setIsEditing(true);
 		}
 	}, [data.label, disabled]);
+	if (node.type === "github" || node.type === "gateway-https" || node.type === "gateway-tcp") {
+		return (
+			<div className="w-full flex flex-row gap-1 items-center">
+				<Icon type={node.type} />
+				<h3 className="w-full text-lg font-bold cursor-text select-none hover:outline-solid hover:outline-2 hover:outline-gray-200">
+					{nodeLabel(node)}
+				</h3>
+			</div>
+		);
+	}
 	return (
 		<div className="w-full flex flex-row gap-1 items-center">
 			<Icon type={node.type} />