Canvas: build application infrastructure with drag and drop

Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/components/node-volume.tsx b/apps/canvas/src/components/node-volume.tsx
new file mode 100644
index 0000000..57c488d
--- /dev/null
+++ b/apps/canvas/src/components/node-volume.tsx
@@ -0,0 +1,126 @@
+import { NodeRect } from './node-rect';
+import { nodeIsConnectable, nodeLabel, useStateStore, VolumeNode } from '@/lib/state';
+import { useEffect, useMemo } from '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 { Handle, Position } from "@xyflow/react";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
+
+export function NodeVolume(node: VolumeNode) {
+  const { id, data, selected } = node;
+  const isConnectable = useMemo(() => nodeIsConnectable(node, "volume"), [node]);
+  return (
+    <NodeRect id={id} selected={selected} type={node.type}>
+      <div style={{ padding: '10px 20px' }}>
+        <div>{nodeLabel(node)}</div>
+        <div>{data.type && `${data.type}`}</div>
+        <div>{data.size && `${data.size}`}</div>
+        <Handle
+          id="volume"
+          type={"source"}
+          position={Position.Top}
+          isConnectableStart={isConnectable}
+          isConnectableEnd={isConnectable}
+          isConnectable={isConnectable}
+         />
+      </div>
+    </NodeRect>
+  );
+}
+
+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({ id, data }: VolumeNode) {
+  const store = useStateStore();
+  const form = useForm<z.infer<typeof schema>>({
+    resolver: zodResolver(schema),
+    mode: "onChange",
+    defaultValues: {
+      name: "",
+      type: undefined,
+      size: "",
+    }
+  });
+  useEffect(() => {
+    const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
+      if (type !== "change") {
+        return
+      }
+      console.log({ name, type, value });
+      store.updateNodeData<"volume">(id, {
+        label: value.name,
+        type: value.type,
+        size: value.size,
+      });
+    });
+    return () => sub.unsubscribe();
+  }, [form, store]);
+  useEffect(() => {
+    form.reset({
+      name: data.label,
+      type: data.type,
+      size: data.size,
+    });
+  }, [form, data])
+  return (
+    <>
+      <Form {...form}>
+        <form className="space-y-2">
+        <FormField 
+            control={form.control}
+            name="name"
+            render={({ field }) => (
+              <FormItem>
+                <FormControl>
+                  <Input placeholder="name" className="border border-black" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        <FormField
+            control={form.control}
+            name="type"
+            render={({ field }) => (
+              <FormItem>
+                <Select onValueChange={field.onChange} defaultValue={field.value}>
+                  <FormControl>
+                    <SelectTrigger>
+                      <SelectValue placeholder="Volume Type" />
+                    </SelectTrigger>
+                  </FormControl>
+                  <SelectContent>
+                    {volumeTypes.map((t) => (
+                      <SelectItem key={t} value={t}>{t}</SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          <FormField 
+            control={form.control}
+            name="size"
+            render={({ field }) => (
+              <FormItem>
+                <FormControl>
+                  <Input placeholder="size" className="border border-black" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </form>
+      </Form>
+    </>);
+}
\ No newline at end of file