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