Canvas: Reuse Name component in node details

Change-Id: Ide8094b50f9ac019e7bada9a000100f9233133da
diff --git a/apps/canvas/front/src/Canvas.tsx b/apps/canvas/front/src/Canvas.tsx
index f8a4b4d..eaaaf70 100644
--- a/apps/canvas/front/src/Canvas.tsx
+++ b/apps/canvas/front/src/Canvas.tsx
@@ -1,6 +1,6 @@
-import { Resources } from "@/components/resources";
-import { Canvas } from "@/components/canvas";
-import { Details } from "@/components/details";
+import { Resources } from "./components/resources";
+import { Canvas } from "./components/canvas";
+import { Details } from "./Details";
 import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./components/ui/resizable";
 import { Tools } from "./Tools";
 import { useStateStore } from "./lib/state";
diff --git a/apps/canvas/front/src/components/details.tsx b/apps/canvas/front/src/Details.tsx
similarity index 86%
rename from apps/canvas/front/src/components/details.tsx
rename to apps/canvas/front/src/Details.tsx
index 9b6cb20..23920dd 100644
--- a/apps/canvas/front/src/components/details.tsx
+++ b/apps/canvas/front/src/Details.tsx
@@ -1,11 +1,11 @@
 import { useNodes } from "@xyflow/react";
 import { AppNode, nodeLabel, NodeType, useMode } from "@/lib/state";
 import { NodeDetails } from "@/components/node-details";
-import { Accordion, AccordionContent, AccordionTrigger } from "./ui/accordion";
+import { Accordion, AccordionContent, AccordionTrigger } from "./components/ui/accordion";
 import { AccordionItem } from "@radix-ui/react-accordion";
 import { useMemo, useState } from "react";
-import { Icon } from "./icon";
-import { Separator } from "./ui/separator";
+import { Separator } from "./components/ui/separator";
+import { Name } from "./components/node-name";
 
 function unique<T>(v: T, i: number, a: T[]) {
 	return a.indexOf(v) === i;
@@ -43,7 +43,6 @@
 	const all = useMemo(() => open.concat(selected).filter(unique), [open, selected]);
 	const mode = useMode();
 	const isDeployMode = mode === "deploy";
-
 	return (
 		<Accordion
 			type="multiple"
@@ -56,13 +55,10 @@
 					{index > 0 && <Separator />}
 					<AccordionItem key={n.id} value={n.id} className="px-1">
 						<AccordionTrigger className="!h-fit">
-							<div className="flex flex-row space-x-2 items-center">
-								<Icon type={n.type} />
-								<span>{nodeLabel(n)}</span>
-							</div>
+							<Name node={n} editing={all.includes(n.id)} />
 						</AccordionTrigger>
 						<AccordionContent className="pt-1">
-							<NodeDetails node={n} disabled={isDeployMode} />
+							<NodeDetails node={n} disabled={isDeployMode} showName={false} />
 						</AccordionContent>
 					</AccordionItem>
 				</>
diff --git a/apps/canvas/front/src/Overview.tsx b/apps/canvas/front/src/Overview.tsx
index 8b5d496..43ba776 100644
--- a/apps/canvas/front/src/Overview.tsx
+++ b/apps/canvas/front/src/Overview.tsx
@@ -1,12 +1,14 @@
 import React, { useMemo } from "react";
-import { useStateStore, ServiceNode } from "@/lib/state";
+import { useStateStore } 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 store = useStateStore();
-	const nodes = useMemo(() => store.nodes, [store.nodes]);
+	const nodes = useMemo(() => {
+		return store.nodes.filter((n) => n.type !== "network" && n.type !== "github");
+	}, [store.nodes]);
 	const isDeployMode = useMemo(() => store.mode === "deploy", [store.mode]);
 	return (
 		<div className="h-full w-full overflow-auto bg-white p-2">
@@ -15,15 +17,13 @@
 				<Canvas className="hidden" />
 			</div>
 			<div className="flex flex-wrap gap-4 pt-2">
-				{nodes
-					.filter((n): n is ServiceNode => n.type === "app")
-					.map((n) => {
-						return (
-							<div key={n.id} className="h-fit w-fit rounded-lg border-gray-200 border-2 p-2">
-								<NodeDetails node={n} disabled={isDeployMode} />
-							</div>
-						);
-					})}
+				{nodes.map((n) => {
+					return (
+						<div key={n.id} className="h-fit w-fit rounded-lg border-gray-200 border-2 p-2">
+							<NodeDetails node={n} disabled={isDeployMode} showName={true} />
+						</div>
+					);
+				})}
 			</div>
 		</div>
 	);
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 71fc358..aaa4ecf 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -30,9 +30,10 @@
 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";
+import { Name } from "./node-name";
+import { NodeDetailsProps } from "@/lib/types";
 
 export function NodeApp(node: ServiceNode) {
 	const { id, selected } = node;
@@ -91,11 +92,11 @@
 	subdomain: z.string().min(1, "required"),
 });
 
-export function NodeAppDetails({ node, disabled }: { node: ServiceNode; disabled?: boolean }) {
+export function NodeAppDetails({ node, disabled, showName = true }: NodeDetailsProps<ServiceNode>) {
 	const { data } = node;
 	return (
 		<>
-			<Name node={node} disabled={disabled} />
+			{showName ? <Name node={node} disabled={disabled} /> : null}
 			<Tabs defaultValue="runtime">
 				<TabsList className="w-full flex flex-row justify-between">
 					<TabsTrigger value="runtime">
@@ -166,46 +167,6 @@
 	);
 }
 
-function Name({ node, disabled }: { node: ServiceNode; disabled?: boolean }): React.ReactNode {
-	const { id, data } = node;
-	const store = useStateStore();
-	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();
diff --git a/apps/canvas/front/src/components/node-details.tsx b/apps/canvas/front/src/components/node-details.tsx
index 39ae059..6267ac3 100644
--- a/apps/canvas/front/src/components/node-details.tsx
+++ b/apps/canvas/front/src/components/node-details.tsx
@@ -1,36 +1,37 @@
 import { NodeAppDetails } from "./node-app";
 import { NodeGatewayHttpsDetails } from "./node-gateway-https";
-import { AppNode } from "@/lib/state";
 import { NodeVolumeDetails } from "./node-volume";
 import { NodePostgreSQLDetails } from "./node-postgresql";
 import { NodeMongoDBDetails } from "./node-mongodb";
 import { NodeGithubDetails } from "./node-github";
 import { NodeGatewayTCPDetails } from "./node-gateway-tcp";
+import { NodeDetailsProps } from "@/lib/types";
 
-export function NodeDetails({ node, disabled }: { node: AppNode; disabled?: boolean }) {
+export function NodeDetails(props: NodeDetailsProps) {
 	return (
 		<div className="px-1 flex flex-col gap-2">
-			<NodeDetailsImpl node={node} disabled={disabled} />
+			<NodeDetailsImpl {...props} />
 		</div>
 	);
 }
 
-function NodeDetailsImpl({ node, disabled }: { node: AppNode; disabled?: boolean }) {
+function NodeDetailsImpl(props: NodeDetailsProps) {
+	const { node, ...rest } = props;
 	switch (node.type) {
 		case "app":
-			return <NodeAppDetails node={node} disabled={disabled} />;
+			return <NodeAppDetails {...rest} node={node} />;
 		case "gateway-https":
-			return <NodeGatewayHttpsDetails node={node} disabled={disabled} />;
+			return <NodeGatewayHttpsDetails {...rest} node={node} />;
 		case "gateway-tcp":
-			return <NodeGatewayTCPDetails node={node} disabled={disabled} />;
+			return <NodeGatewayTCPDetails {...rest} node={node} />;
 		case "volume":
-			return <NodeVolumeDetails node={node} disabled={disabled} />;
+			return <NodeVolumeDetails {...rest} node={node} />;
 		case "postgresql":
-			return <NodePostgreSQLDetails node={node} disabled={disabled} />;
+			return <NodePostgreSQLDetails {...rest} node={node} />;
 		case "mongodb":
-			return <NodeMongoDBDetails node={node} disabled={disabled} />;
+			return <NodeMongoDBDetails {...rest} node={node} />;
 		case "github":
-			return <NodeGithubDetails node={node} disabled={disabled} />;
+			return <NodeGithubDetails {...rest} node={node} />;
 		default:
 			return <>nooo</>;
 	}
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index 6efc356..fe17527 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -21,6 +21,7 @@
 import { Button } from "./ui/button";
 import { XIcon } from "lucide-react";
 import { Switch } from "./ui/switch";
+import { NodeDetailsProps } from "@/lib/types";
 
 const schema = z.object({
 	network: z.string().min(1, "reqired"),
@@ -71,7 +72,7 @@
 	);
 }
 
-export function NodeGatewayHttpsDetails({ node, disabled }: { node: GatewayHttpsNode; disabled?: boolean }) {
+export function NodeGatewayHttpsDetails({ node, disabled }: NodeDetailsProps<GatewayHttpsNode>) {
 	const { id, data } = node;
 	const store = useStateStore();
 	const env = useEnv();
diff --git a/apps/canvas/front/src/components/node-gateway-tcp.tsx b/apps/canvas/front/src/components/node-gateway-tcp.tsx
index 6bb4577..919bf83 100644
--- a/apps/canvas/front/src/components/node-gateway-tcp.tsx
+++ b/apps/canvas/front/src/components/node-gateway-tcp.tsx
@@ -10,6 +10,7 @@
 import { Input } from "./ui/input";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
 import { Button } from "./ui/button";
+import { NodeDetailsProps } from "@/lib/types";
 
 const schema = z.object({
 	network: z.string().min(1, "reqired"),
@@ -48,7 +49,7 @@
 	);
 }
 
-export function NodeGatewayTCPDetails({ node, disabled }: { node: GatewayTCPNode; disabled?: boolean }) {
+export function NodeGatewayTCPDetails({ node, disabled }: NodeDetailsProps<GatewayTCPNode>) {
 	const { id, data } = node;
 	const store = useStateStore();
 	const env = useEnv();
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index d16a191..b75f7b1 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -28,6 +28,7 @@
 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";
 
 export function NodeGithub(node: GithubNode) {
 	const { id, selected } = node;
@@ -53,7 +54,7 @@
 	repositoryId: z.number().optional(),
 });
 
-export function NodeGithubDetails({ node, disabled }: { node: GithubNode; disabled?: boolean }) {
+export function NodeGithubDetails({ node, disabled }: NodeDetailsProps<GithubNode>) {
 	const { id, data } = node;
 	const store = useStateStore();
 	const projectId = useProjectId();
diff --git a/apps/canvas/front/src/components/node-mongodb.tsx b/apps/canvas/front/src/components/node-mongodb.tsx
index 3235d41..8b9e53b 100644
--- a/apps/canvas/front/src/components/node-mongodb.tsx
+++ b/apps/canvas/front/src/components/node-mongodb.tsx
@@ -1,12 +1,8 @@
 import { NodeRect } from "./node-rect";
-import { nodeLabel, MongoDBNode, useStateStore } from "@/lib/state";
-import { useEffect } from "react";
+import { nodeLabel, MongoDBNode } from "@/lib/state";
 import { Handle, Position } from "@xyflow/react";
-import { z } from "zod";
-import { DeepPartial, EventType, useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
-import { Input } from "./ui/input";
+import { Name } from "./node-name";
+import { NodeDetailsProps } from "@/lib/types";
 
 export function NodeMongoDB(node: MongoDBNode) {
 	const { id, selected } = node;
@@ -27,54 +23,6 @@
 	);
 }
 
-const schema = z.object({
-	name: z.string().min(1, "required"),
-});
-
-export function NodeMongoDBDetails({ node, disabled }: { node: MongoDBNode; disabled?: boolean }) {
-	const { id, data } = node;
-	const store = useStateStore();
-	const form = useForm<z.infer<typeof schema>>({
-		resolver: zodResolver(schema),
-		mode: "onChange",
-		defaultValues: {
-			name: data.label,
-		},
-	});
-	useEffect(() => {
-		const sub = form.watch(
-			(
-				value: DeepPartial<z.infer<typeof schema>>,
-				{ type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
-			) => {
-				if (type !== "change") {
-					return;
-				}
-				store.updateNodeData<"mongodb">(id, {
-					label: value.name,
-				});
-			},
-		);
-		return () => sub.unsubscribe();
-	}, [id, form, store]);
-	return (
-		<>
-			<Form {...form}>
-				<form className="space-y-2">
-					<FormField
-						control={form.control}
-						name="name"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="name" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-				</form>
-			</Form>
-		</>
-	);
+export function NodeMongoDBDetails({ node, disabled, showName = true }: NodeDetailsProps<MongoDBNode>) {
+	return showName ? <Name node={node} disabled={disabled} /> : null;
 }
diff --git a/apps/canvas/front/src/components/node-name.tsx b/apps/canvas/front/src/components/node-name.tsx
new file mode 100644
index 0000000..7a68f83
--- /dev/null
+++ b/apps/canvas/front/src/components/node-name.tsx
@@ -0,0 +1,50 @@
+import { useState, useEffect } from "react";
+import { useStateStore } from "@/lib/state";
+import { AppNode } from "@/lib/state";
+import { Icon } from "./icon";
+import { Input } from "./ui/input";
+
+export function Name({
+	node,
+	disabled,
+	editing,
+}: {
+	node: AppNode;
+	disabled?: boolean;
+	editing?: boolean;
+}): React.ReactNode {
+	const { id, data } = node;
+	const store = useStateStore();
+	const [isEditing, setIsEditing] = useState(false);
+	useEffect(() => {
+		if (data.label === "") {
+			setIsEditing(true);
+		}
+	}, [data.label, disabled]);
+	return (
+		<div className="w-full flex flex-row gap-1 items-center">
+			<Icon type={node.type} />
+			{isEditing || editing ? (
+				<Input
+					placeholder="Name"
+					className="w-full"
+					value={data.label}
+					onChange={(e) => store.updateNodeData(id, { label: e.target.value })}
+					onBlur={() => {
+						if (data.label !== "") {
+							setIsEditing(false);
+						}
+					}}
+					disabled={disabled}
+				/>
+			) : (
+				<h3
+					className="w-full text-lg font-bold cursor-text select-none hover:outline-solid hover:outline-2 hover:outline-gray-200"
+					onClick={() => setIsEditing(true)}
+				>
+					{data.label}
+				</h3>
+			)}
+		</div>
+	);
+}
diff --git a/apps/canvas/front/src/components/node-postgresql.tsx b/apps/canvas/front/src/components/node-postgresql.tsx
index 140fe4d..0ae86a1 100644
--- a/apps/canvas/front/src/components/node-postgresql.tsx
+++ b/apps/canvas/front/src/components/node-postgresql.tsx
@@ -1,12 +1,8 @@
 import { NodeRect } from "./node-rect";
-import { nodeLabel, PostgreSQLNode, useStateStore } from "@/lib/state";
-import { useEffect } from "react";
+import { nodeLabel, PostgreSQLNode } from "@/lib/state";
 import { Handle, Position } from "@xyflow/react";
-import { z } from "zod";
-import { DeepPartial, EventType, useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
-import { Input } from "./ui/input";
+import { Name } from "./node-name";
+import { NodeDetailsProps } from "@/lib/types";
 
 export function NodePostgreSQL(node: PostgreSQLNode) {
 	const { id, selected } = node;
@@ -27,54 +23,6 @@
 	);
 }
 
-const schema = z.object({
-	name: z.string().min(1, "required"),
-});
-
-export function NodePostgreSQLDetails({ node, disabled }: { node: PostgreSQLNode; disabled?: boolean }) {
-	const { id, data } = node;
-	const store = useStateStore();
-	const form = useForm<z.infer<typeof schema>>({
-		resolver: zodResolver(schema),
-		mode: "onChange",
-		defaultValues: {
-			name: data.label,
-		},
-	});
-	useEffect(() => {
-		const sub = form.watch(
-			(
-				value: DeepPartial<z.infer<typeof schema>>,
-				{ type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
-			) => {
-				if (type !== "change") {
-					return;
-				}
-				store.updateNodeData<"postgresql">(id, {
-					label: value.name,
-				});
-			},
-		);
-		return () => sub.unsubscribe();
-	}, [id, form, store]);
-	return (
-		<>
-			<Form {...form}>
-				<form className="space-y-2">
-					<FormField
-						control={form.control}
-						name="name"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="name" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-				</form>
-			</Form>
-		</>
-	);
+export function NodePostgreSQLDetails({ node, disabled, showName = true }: NodeDetailsProps<PostgreSQLNode>) {
+	return showName ? <Name node={node} disabled={disabled} /> : null;
 }
diff --git a/apps/canvas/front/src/components/node-volume.tsx b/apps/canvas/front/src/components/node-volume.tsx
index 35b3722..39bf15a 100644
--- a/apps/canvas/front/src/components/node-volume.tsx
+++ b/apps/canvas/front/src/components/node-volume.tsx
@@ -8,6 +8,8 @@
 import { Input } from "./ui/input";
 import { Handle, Position } from "@xyflow/react";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
+import { Name } from "./node-name";
+import { NodeDetailsProps } from "@/lib/types";
 
 export function NodeVolume(node: VolumeNode) {
 	const { id, data, selected } = node;
@@ -34,19 +36,17 @@
 const volumeTypes = ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"] as const;
 
 const schema = z.object({
-	name: z.string().min(1),
 	type: z.enum(volumeTypes),
 	size: z.string().min(1).default("1Gi"),
 });
 
-export function NodeVolumeDetails({ node, disabled }: { node: VolumeNode; disabled?: boolean }) {
+export function NodeVolumeDetails({ node, disabled, showName = true }: NodeDetailsProps<VolumeNode>) {
 	const { id, data } = node;
 	const store = useStateStore();
 	const form = useForm<z.infer<typeof schema>>({
 		resolver: zodResolver(schema),
 		mode: "onChange",
 		defaultValues: {
-			name: "",
 			type: undefined,
 			size: "",
 		},
@@ -62,7 +62,6 @@
 				}
 				console.log({ name, type, value });
 				store.updateNodeData<"volume">(id, {
-					label: value.name,
 					type: value.type,
 					size: value.size,
 				});
@@ -72,29 +71,17 @@
 	}, [id, form, store]);
 	useEffect(() => {
 		form.reset({
-			name: data.label,
 			type: data.type,
 			size: data.size,
 		});
 	}, [form, data]);
 	return (
 		<>
+			{showName ? <Name node={node} disabled={disabled} /> : null}
 			<Form {...form}>
 				<form className="space-y-2">
 					<FormField
 						control={form.control}
-						name="name"
-						render={({ field }) => (
-							<FormItem>
-								<FormControl>
-									<Input placeholder="name" {...field} disabled={disabled} />
-								</FormControl>
-								<FormMessage />
-							</FormItem>
-						)}
-					/>
-					<FormField
-						control={form.control}
 						name="type"
 						render={({ field }) => (
 							<FormItem>
diff --git a/apps/canvas/front/src/lib/types.ts b/apps/canvas/front/src/lib/types.ts
new file mode 100644
index 0000000..e692991
--- /dev/null
+++ b/apps/canvas/front/src/lib/types.ts
@@ -0,0 +1,7 @@
+import { AppNode } from "./state";
+
+export interface NodeDetailsProps<T extends AppNode = AppNode> {
+	node: T;
+	disabled?: boolean;
+	showName?: boolean;
+}