blob: 1397f5cd70b81a45109d86c05581bdac1312fd9f [file] [log] [blame]
gioaba9a962025-04-25 14:19:40 +00001import { v4 as uuidv4 } from "uuid";
gio5f2f1002025-03-20 18:38:48 +04002import { useStateStore, AppNode, GatewayHttpsNode, ServiceNode, nodeLabel, useEnv, nodeIsConnectable } from '@/lib/state';
3import { Handle, Position, useNodes } from '@xyflow/react';
4import { NodeRect } from './node-rect';
5import { useEffect, useMemo } from 'react';
6import { z } from "zod";
7import { zodResolver } from "@hookform/resolvers/zod";
8import { useForm, EventType, DeepPartial } from 'react-hook-form';
9import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
10import { Input } from './ui/input';
11import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
12
13const schema = z.object({
14 network: z.string().min(1, "reqired"),
15 subdomain: z.string().min(1, "required"),
16});
17
18const connectedToSchema = z.object({
19 id: z.string(),
20 portId: z.string(),
21});
22
23export function NodeGatewayHttps(node: GatewayHttpsNode) {
24 const { id, selected } = node;
gioaba9a962025-04-25 14:19:40 +000025 const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
gio5f2f1002025-03-20 18:38:48 +040026 const isConnectable = useMemo(() => nodeIsConnectable(node, "https"), [node]);
27 return (
gio1dc800a2025-04-24 17:15:43 +000028 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
gio5f2f1002025-03-20 18:38:48 +040029 {nodeLabel(node)}
30 <Handle
gioaba9a962025-04-25 14:19:40 +000031 type={"source"}
32 id="subdomain"
33 position={Position.Top}
34 isConnectable={isConnectableNetwork}
35 isConnectableStart={isConnectableNetwork}
36 isConnectableEnd={isConnectableNetwork}
37 />
38 <Handle
gio5f2f1002025-03-20 18:38:48 +040039 type={"target"}
40 id="https"
41 position={Position.Bottom}
42 isConnectable={isConnectable}
43 isConnectableStart={isConnectable}
44 isConnectableEnd={isConnectable}
45 />
46 </NodeRect>
47 );
48}
49
50export function NodeGatewayHttpsDetails({ id, data }: GatewayHttpsNode) {
51 const store = useStateStore();
52 const env = useEnv();
53 const form = useForm<z.infer<typeof schema>>({
54 resolver: zodResolver(schema),
55 mode: "onChange",
56 defaultValues: {
giof96ffb82025-04-24 09:31:05 +000057 network: data.network,
58 subdomain: data.subdomain,
gio5f2f1002025-03-20 18:38:48 +040059 },
60 });
61 useEffect(() => {
62 const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
63 if (name === "network") {
gioaba9a962025-04-25 14:19:40 +000064 let edges = store.edges;
65 if (data.network !== undefined) {
66 edges = edges.filter((e) => {
67 console.log(e);
68 if (e.source === id && e.sourceHandle === "subdomain" && e.target === data.network && e.targetHandle === "subdomain") {
69 return false;
70 } else {
71 return true;
72 }
73 });
gio5f2f1002025-03-20 18:38:48 +040074 }
gioaba9a962025-04-25 14:19:40 +000075 if (value.network !== undefined) {
76 edges = edges.concat({
77 id: uuidv4(),
78 source: id,
79 sourceHandle: "subdomain",
80 target: value.network,
81 targetHandle: "subdomain",
82 });
83 }
84 store.setEdges(edges);
85 store.updateNodeData<"gateway-https">(id, { network: value.network });
gio5f2f1002025-03-20 18:38:48 +040086 } else if (name === "subdomain") {
87 store.updateNodeData<"gateway-https">(id, { subdomain: value.subdomain });
88 }
89 });
90 return () => sub.unsubscribe();
gioaba9a962025-04-25 14:19:40 +000091 }, [id, data, form, store]);
gio5f2f1002025-03-20 18:38:48 +040092 const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
93 resolver: zodResolver(connectedToSchema),
94 mode: "onChange",
95 defaultValues: {
96 id: data.https?.serviceId,
97 portId: data.https?.portId,
98 },
99 });
100 useEffect(() => {
101 connectedToForm.reset({
102 id: data.https?.serviceId,
103 portId: data.https?.portId,
104 });
105 }, [connectedToForm, data]);
106 const nodes = useNodes<AppNode>();
107 const selected = useMemo(() => {
108 if (data !== undefined && data.https !== undefined) {
109 const https = data.https;
110 return nodes.find((n) => n.id === https.serviceId)! as ServiceNode;
111 }
112 return null;
113 }, [data]);
114 const selectable = useMemo(() => {
115 return nodes.filter((n) => {
116 if (n.id === id) {
117 return false;
118 }
119 if (selected !== null && selected.id === id) {
120 return true;
121 }
122 if (n.type !== "app") {
123 return false;
124 }
125 return n.data && n.data.ports && n.data.ports.length > 0;
126 })
127 }, [nodes, selected]);
128 useEffect(() => {
129 const sub = connectedToForm.watch((value: DeepPartial<z.infer<typeof connectedToSchema>>, { name, type }: { name?: keyof z.infer<typeof connectedToSchema> | undefined, type?: EventType | undefined }) => {
130 console.log({ name, type });
131 if (type !== "change") {
132 return;
133 }
134 switch (name) {
135 case "id":
136 if (!value.id) {
137 break;
138 }
139 const current = store.edges.filter((e) => e.target === id);
140 const cid = current[0] ? current[0].id : undefined;
141 store.replaceEdge({
142 source: value.id,
143 sourceHandle: "ports",
144 target: id,
145 targetHandle: "https",
146 }, cid);
147 break;
148 case "portId":
149 store.updateNodeData<"gateway-https">(id, {
150 https: {
151 serviceId: value.id,
152 portId: value.portId,
153 }
154 });
155 break;
156 }
157 });
158 return () => sub.unsubscribe();
159 }, [connectedToForm, store, selectable]);
160 return (
161 <>
162 <Form {...form}>
163 <form className="space-y-2">
164 <FormField
165 control={form.control}
166 name="network"
167 render={({ field }) => (
168 <FormItem>
169 <Select onValueChange={field.onChange} defaultValue={field.value}>
170 <FormControl>
171 <SelectTrigger>
172 <SelectValue placeholder="Network" />
173 </SelectTrigger>
174 </FormControl>
175 <SelectContent>
176 {env.networks.map((n) => (
177 <SelectItem key={n.name} value={n.domain}>{`${n.name} - ${n.domain}`}</SelectItem>
178 ))}
179 </SelectContent>
180 </Select>
181 <FormMessage />
182 </FormItem>
183 )}
184 />
185 <FormField
186 control={form.control}
187 name="subdomain"
188 render={({ field }) => (
189 <FormItem>
190 <FormControl>
191 <Input placeholder="subdomain" className="border border-black" {...field} />
192 </FormControl>
193 <FormMessage />
194 </FormItem>
195 )}
196 />
197 </form>
198 </Form>
199 <Form {...connectedToForm}>
200 <form className="space-y-2">
201 <FormField
202 control={connectedToForm.control}
203 name="id"
204 render={({ field }) => (
205 <FormItem>
206 <Select onValueChange={field.onChange} defaultValue={field.value}>
207 <FormControl>
208 <SelectTrigger>
209 <SelectValue placeholder="Service" />
210 </SelectTrigger>
211 </FormControl>
212 <SelectContent>
213 {selectable.map((n) => (
214 <SelectItem key={n.id} value={n.id}>{nodeLabel(n)}</SelectItem>
215 ))}
216 </SelectContent>
217 </Select>
218 <FormMessage />
219 </FormItem>
220 )}
221 />
222 <FormField
223 control={connectedToForm.control}
224 name="portId"
225 render={({ field }) => (
226 <FormItem>
227 <Select onValueChange={field.onChange} defaultValue={field.value}>
228 <FormControl>
229 <SelectTrigger>
230 <SelectValue placeholder="Port" />
231 </SelectTrigger>
232 </FormControl>
233 <SelectContent>
234 {selected && selected.data.ports.map((p) => (
235 <SelectItem key={p.id} value={p.id}>{p.name} - {p.value}</SelectItem>
236 ))}
237 </SelectContent>
238 </Select>
239 <FormMessage />
240 </FormItem>
241 )}
242 />
243 </form>
244 </Form>
245 </>
246 );
247}