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} />