Canvas: build application infrastructure with drag and drop
Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/components/node-gateway-https.tsx b/apps/canvas/src/components/node-gateway-https.tsx
new file mode 100644
index 0000000..cd9e06c
--- /dev/null
+++ b/apps/canvas/src/components/node-gateway-https.tsx
@@ -0,0 +1,220 @@
+import { useStateStore, AppNode, GatewayHttpsNode, ServiceNode, nodeLabel, useEnv, nodeIsConnectable } from '@/lib/state';
+import { Handle, Position, useNodes } from '@xyflow/react';
+import { NodeRect } from './node-rect';
+import { useEffect, useMemo } from 'react';
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm, EventType, DeepPartial } from 'react-hook-form';
+import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
+import { Input } from './ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
+
+const schema = z.object({
+ network: z.string().min(1, "reqired"),
+ subdomain: z.string().min(1, "required"),
+});
+
+const connectedToSchema = z.object({
+ id: z.string(),
+ portId: z.string(),
+});
+
+export function NodeGatewayHttps(node: GatewayHttpsNode) {
+ const { id, selected } = node;
+ const isConnectable = useMemo(() => nodeIsConnectable(node, "https"), [node]);
+ return (
+ <NodeRect id={id} selected={selected} type={node.type}>
+ {nodeLabel(node)}
+ <Handle
+ type={"target"}
+ id="https"
+ position={Position.Bottom}
+ isConnectable={isConnectable}
+ isConnectableStart={isConnectable}
+ isConnectableEnd={isConnectable}
+ />
+ </NodeRect>
+ );
+}
+
+export function NodeGatewayHttpsDetails({ id, data }: GatewayHttpsNode) {
+ const store = useStateStore();
+ const env = useEnv();
+ const form = useForm<z.infer<typeof schema>>({
+ resolver: zodResolver(schema),
+ mode: "onChange",
+ defaultValues: {
+ network: "",
+ subdomain: "",
+ },
+ });
+ useEffect(() => {
+ const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
+ if (name === "network") {
+ if (value.network !== undefined) {
+ store.updateNodeData<"gateway-https">(id, { network: value.network });
+ } else {
+
+ }
+ } else if (name === "subdomain") {
+ store.updateNodeData<"gateway-https">(id, { subdomain: value.subdomain });
+ }
+ });
+ return () => sub.unsubscribe();
+ }, [form, store]);
+ const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
+ resolver: zodResolver(connectedToSchema),
+ mode: "onChange",
+ defaultValues: {
+ id: data.https?.serviceId,
+ portId: data.https?.portId,
+ },
+ });
+ useEffect(() => {
+ connectedToForm.reset({
+ id: data.https?.serviceId,
+ portId: data.https?.portId,
+ });
+ }, [connectedToForm, data]);
+ const nodes = useNodes<AppNode>();
+ const selected = useMemo(() => {
+ if (data !== undefined && data.https !== undefined) {
+ const https = data.https;
+ return nodes.find((n) => n.id === https.serviceId)! as ServiceNode;
+ }
+ return null;
+ }, [data]);
+ const selectable = useMemo(() => {
+ return nodes.filter((n) => {
+ if (n.id === id) {
+ return false;
+ }
+ if (selected !== null && selected.id === id) {
+ return true;
+ }
+ if (n.type !== "app") {
+ return false;
+ }
+ return n.data && n.data.ports && n.data.ports.length > 0;
+ })
+ }, [nodes, selected]);
+ useEffect(() => {
+ const sub = connectedToForm.watch((value: DeepPartial<z.infer<typeof connectedToSchema>>, { name, type }: { name?: keyof z.infer<typeof connectedToSchema> | undefined, type?: EventType | undefined }) => {
+ console.log({ name, type });
+ if (type !== "change") {
+ return;
+ }
+ switch (name) {
+ case "id":
+ if (!value.id) {
+ break;
+ }
+ const current = store.edges.filter((e) => e.target === id);
+ const cid = current[0] ? current[0].id : undefined;
+ store.replaceEdge({
+ source: value.id,
+ sourceHandle: "ports",
+ target: id,
+ targetHandle: "https",
+ }, cid);
+ break;
+ case "portId":
+ store.updateNodeData<"gateway-https">(id, {
+ https: {
+ serviceId: value.id,
+ portId: value.portId,
+ }
+ });
+ break;
+ }
+ });
+ return () => sub.unsubscribe();
+ }, [connectedToForm, store, selectable]);
+ return (
+ <>
+ <Form {...form}>
+ <form className="space-y-2">
+ <FormField
+ control={form.control}
+ name="network"
+ render={({ field }) => (
+ <FormItem>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Network" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {env.networks.map((n) => (
+ <SelectItem key={n.name} value={n.domain}>{`${n.name} - ${n.domain}`}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="subdomain"
+ render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <Input placeholder="subdomain" className="border border-black" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </form>
+ </Form>
+ <Form {...connectedToForm}>
+ <form className="space-y-2">
+ <FormField
+ control={connectedToForm.control}
+ name="id"
+ render={({ field }) => (
+ <FormItem>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Service" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {selectable.map((n) => (
+ <SelectItem key={n.id} value={n.id}>{nodeLabel(n)}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={connectedToForm.control}
+ name="portId"
+ render={({ field }) => (
+ <FormItem>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Port" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {selected && selected.data.ports.map((p) => (
+ <SelectItem key={p.id} value={p.id}>{p.name} - {p.value}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </form>
+ </Form>
+ </>
+ );
+}
\ No newline at end of file