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}
+ />
</>
);
}