Canvas: Import modal

Change-Id: I22928007c5b81d93be2eed2d133fed4d73e1703f
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}
+			/>
 		</>
 	);
 }