blob: c37f31b959afe6e64824e5fc31b28f7997183e1e [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { v4 as uuidv4 } from "uuid";
giod0026612025-05-08 13:00:36 +00002import { useStateStore, AppNode, nodeLabel, useEnv, GatewayTCPNode, nodeIsConnectable } from "@/lib/state";
3import { Edge, Handle, Position, useNodes } from "@xyflow/react";
4import { NodeRect } from "./node-rect";
5import { useCallback, useEffect, useMemo, useState } from "react";
gio5f2f1002025-03-20 18:38:48 +04006import { z } from "zod";
7import { zodResolver } from "@hookform/resolvers/zod";
giod0026612025-05-08 13:00:36 +00008import { 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";
gio5f2f1002025-03-20 18:38:48 +040012import { Button } from "./ui/button";
13
14const schema = z.object({
giod0026612025-05-08 13:00:36 +000015 network: z.string().min(1, "reqired"),
16 subdomain: z.string().min(1, "required"),
gio5f2f1002025-03-20 18:38:48 +040017});
18
19const connectedToSchema = z.object({
giod0026612025-05-08 13:00:36 +000020 serviceId: z.string(),
21 portId: z.string(),
gio5f2f1002025-03-20 18:38:48 +040022});
23
24export function NodeGatewayTCP(node: GatewayTCPNode) {
giod0026612025-05-08 13:00:36 +000025 const { id, selected } = node;
26 const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
27 const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
28 return (
29 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
30 {nodeLabel(node)}
31 <Handle
32 type={"source"}
33 id="subdomain"
34 position={Position.Top}
35 isConnectable={isConnectableNetwork}
36 isConnectableStart={isConnectableNetwork}
37 isConnectableEnd={isConnectableNetwork}
38 />
39 <Handle
40 type={"target"}
41 id="tcp"
42 position={Position.Bottom}
43 isConnectable={isConnectable}
44 isConnectableStart={isConnectable}
45 isConnectableEnd={isConnectable}
46 />
47 </NodeRect>
48 );
gio5f2f1002025-03-20 18:38:48 +040049}
50
gio3ec94242025-05-16 12:46:57 +000051export function NodeGatewayTCPDetails({ id, data, disabled }: GatewayTCPNode & { disabled?: boolean }) {
giod0026612025-05-08 13:00:36 +000052 const store = useStateStore();
53 const env = useEnv();
54 const form = useForm<z.infer<typeof schema>>({
55 resolver: zodResolver(schema),
56 mode: "onChange",
57 defaultValues: {
58 network: data.network,
59 subdomain: data.subdomain,
60 },
61 });
62 useEffect(() => {
63 const sub = form.watch(
64 (
65 value: DeepPartial<z.infer<typeof schema>>,
66 { name }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
67 ) => {
68 if (name === "network") {
69 let edges = store.edges;
70 if (data.network !== undefined) {
71 edges = edges.filter((e) => {
72 console.log(e);
73 if (
74 e.source === id &&
75 e.sourceHandle === "subdomain" &&
76 e.target === data.network &&
77 e.targetHandle === "subdomain"
78 ) {
79 return false;
80 } else {
81 return true;
82 }
83 });
84 }
85 if (value.network !== undefined) {
86 edges = edges.concat({
87 id: uuidv4(),
88 source: id,
89 sourceHandle: "subdomain",
90 target: value.network,
91 targetHandle: "subdomain",
92 });
93 }
94 store.setEdges(edges);
95 store.updateNodeData<"gateway-tcp">(id, { network: value.network });
96 } else if (name === "subdomain") {
97 store.updateNodeData<"gateway-tcp">(id, { subdomain: value.subdomain });
98 }
99 },
100 );
101 return () => sub.unsubscribe();
102 }, [id, data, form, store]);
103 const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
104 resolver: zodResolver(connectedToSchema),
105 mode: "onSubmit",
106 defaultValues: {
107 serviceId: data.selected?.serviceId,
108 portId: data.selected?.portId,
109 },
110 });
111 useEffect(() => {
112 connectedToForm.reset({
113 serviceId: data.selected?.serviceId,
114 portId: data.selected?.portId,
115 });
116 console.log(connectedToForm.getValues());
117 }, [id, connectedToForm, data]);
118 const nodes = useNodes<AppNode>();
119 const [selected, setSelected] = useState<AppNode | undefined>(undefined);
120 useEffect(() => {
121 if (data.selected?.serviceId == null) {
122 setSelected(undefined);
123 } else {
124 const serviceId = data.selected.serviceId;
125 setSelected(nodes.find((n) => n.id === serviceId));
126 }
127 }, [id, data, setSelected, nodes]);
128 const selectable = useMemo(() => {
129 console.log(selected);
130 return nodes.filter((n) => {
131 if (n.id === id) {
132 return false;
133 }
134 if (selected != null && selected.id === id) {
135 return true;
136 }
137 if ("ports" in n.data && (n.data.ports || []).length > 0) {
138 return true;
139 }
140 return false;
141 });
142 }, [id, nodes, selected]);
143 useEffect(() => {
144 const sub = connectedToForm.watch(
145 (
146 value: DeepPartial<z.infer<typeof connectedToSchema>>,
147 {
148 name,
149 type,
150 }: { name?: keyof z.infer<typeof connectedToSchema> | undefined; type?: EventType | undefined },
151 ) => {
152 if (type !== "change") {
153 return;
154 }
155 switch (name) {
156 case "serviceId":
157 if (!value.serviceId) {
158 break;
159 }
160 store.updateNodeData<"gateway-tcp">(id, {
161 selected: {
162 serviceId: value.serviceId,
163 },
164 });
165 break;
166 case "portId":
167 if (!value.portId) {
168 break;
169 }
170 store.updateNodeData<"gateway-tcp">(id, {
171 selected: {
172 serviceId: value.serviceId,
173 portId: value.portId,
174 },
175 });
176 break;
177 }
178 },
179 );
180 return () => sub.unsubscribe();
181 }, [id, connectedToForm, store]);
182 const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
183 const [portLabels, setPortLabels] = useState(new Map<string, string>());
184 useEffect(() => {
185 setNodeLabels(
186 new Map(
187 (data.exposed || []).map((e) => [e.serviceId, nodeLabel(nodes.find((n) => n.id === e.serviceId)!)]),
188 ),
189 );
190 setPortLabels(
191 new Map(
192 (data.exposed || []).map((e) => [
193 `${e.serviceId} - ${e.portId}`,
194 (nodes.find((n) => n.id === e.serviceId)!.data.ports || []).find((p) => p.id === e.portId)!.name,
195 ]),
196 ),
197 );
198 }, [nodes, data, setNodeLabels, setPortLabels]);
199 const onSubmit = useCallback(
200 (values: z.infer<typeof connectedToSchema>) => {
201 const edges = store.edges.filter((e) => e.target !== id);
202 const exp = (data.exposed || []).concat({
203 serviceId: values.serviceId,
204 portId: values.portId,
205 });
206 store.updateNodeData<"gateway-tcp">(id, {
207 exposed: exp,
208 selected: undefined,
209 });
210 store.setEdges(
211 edges.concat(
gio97efd722025-05-19 10:36:12 +0000212 exp.map((e): Edge => {
213 const sn = nodes.find((n) => n.id === e.serviceId);
214 if (sn == null) {
215 throw new Error(`Service ${e.serviceId} not found`);
216 }
217 if (sn.type === "app") {
218 return {
219 id: uuidv4(),
220 source: e.serviceId,
221 sourceHandle: "ports",
222 target: id,
223 targetHandle: "tcp",
224 };
225 } else {
226 return {
227 id: uuidv4(),
228 source: e.serviceId,
229 sourceHandle: "env_var",
230 target: id,
231 targetHandle: "tcp",
232 };
233 }
234 }),
giod0026612025-05-08 13:00:36 +0000235 ),
236 );
237 },
238 [id, data, store],
239 );
240 return (
241 <>
242 <Form {...form}>
243 <form className="space-y-2">
244 <FormField
245 control={form.control}
246 name="network"
247 render={({ field }) => (
248 <FormItem>
gio48fde052025-05-14 09:48:08 +0000249 <Select
250 onValueChange={field.onChange}
251 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000252 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000253 >
giod0026612025-05-08 13:00:36 +0000254 <FormControl>
255 <SelectTrigger>
256 <SelectValue placeholder="Network" />
257 </SelectTrigger>
258 </FormControl>
259 <SelectContent>
260 {env.networks.map((n) => (
261 <SelectItem
262 key={n.name}
263 value={n.domain}
264 >{`${n.name} - ${n.domain}`}</SelectItem>
265 ))}
266 </SelectContent>
267 </Select>
268 <FormMessage />
269 </FormItem>
270 )}
271 />
272 <FormField
273 control={form.control}
274 name="subdomain"
275 render={({ field }) => (
276 <FormItem>
277 <FormControl>
gio3ec94242025-05-16 12:46:57 +0000278 <Input placeholder="subdomain" {...field} disabled={data.readonly || disabled} />
giod0026612025-05-08 13:00:36 +0000279 </FormControl>
280 <FormMessage />
281 </FormItem>
282 )}
283 />
284 </form>
285 </Form>
286 Exposed Services
287 <ul>
288 {(data.exposed || []).map((e, i) => (
289 <li key={i}>
290 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
291 </li>
292 ))}
293 </ul>
294 <Form {...connectedToForm}>
295 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
296 <FormField
297 control={connectedToForm.control}
298 name="serviceId"
299 render={({ field }) => (
300 <FormItem>
gio48fde052025-05-14 09:48:08 +0000301 <Select
302 onValueChange={field.onChange}
303 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000304 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000305 >
giod0026612025-05-08 13:00:36 +0000306 <FormControl>
307 <SelectTrigger>
308 <SelectValue placeholder="Service" />
309 </SelectTrigger>
310 </FormControl>
311 <SelectContent>
312 {selectable.map((n) => (
313 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
314 ))}
315 </SelectContent>
316 </Select>
317 <FormMessage />
318 </FormItem>
319 )}
320 />
321 <FormField
322 control={connectedToForm.control}
323 name="portId"
324 render={({ field }) => (
325 <FormItem>
gio48fde052025-05-14 09:48:08 +0000326 <Select
327 onValueChange={field.onChange}
328 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000329 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000330 >
giod0026612025-05-08 13:00:36 +0000331 <FormControl>
332 <SelectTrigger>
333 <SelectValue placeholder="Port" />
334 </SelectTrigger>
335 </FormControl>
336 <SelectContent>
337 {selected &&
338 (selected.data.ports || []).map((p) => (
339 <SelectItem key={p.id} value={p.id}>
340 {p.name} - {p.value}
341 </SelectItem>
342 ))}
343 </SelectContent>
344 </Select>
345 <FormMessage />
346 </FormItem>
347 )}
348 />
gio3ec94242025-05-16 12:46:57 +0000349 <Button type="submit" disabled={disabled}>
350 Expose
351 </Button>
giod0026612025-05-08 13:00:36 +0000352 </form>
353 </Form>
354 </>
355 );
356}