blob: 6bb4577875db3f0a7c85791a9032db06fcc0ba1d [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
gio08acd3a2025-06-12 12:15:30 +000051export function NodeGatewayTCPDetails({ node, disabled }: { node: GatewayTCPNode; disabled?: boolean }) {
52 const { id, data } = node;
giod0026612025-05-08 13:00:36 +000053 const store = useStateStore();
54 const env = useEnv();
55 const form = useForm<z.infer<typeof schema>>({
56 resolver: zodResolver(schema),
57 mode: "onChange",
58 defaultValues: {
59 network: data.network,
60 subdomain: data.subdomain,
61 },
62 });
63 useEffect(() => {
64 const sub = form.watch(
65 (
66 value: DeepPartial<z.infer<typeof schema>>,
67 { name }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
68 ) => {
69 if (name === "network") {
70 let edges = store.edges;
71 if (data.network !== undefined) {
72 edges = edges.filter((e) => {
73 console.log(e);
74 if (
75 e.source === id &&
76 e.sourceHandle === "subdomain" &&
77 e.target === data.network &&
78 e.targetHandle === "subdomain"
79 ) {
80 return false;
81 } else {
82 return true;
83 }
84 });
85 }
86 if (value.network !== undefined) {
87 edges = edges.concat({
88 id: uuidv4(),
89 source: id,
90 sourceHandle: "subdomain",
91 target: value.network,
92 targetHandle: "subdomain",
93 });
94 }
95 store.setEdges(edges);
96 store.updateNodeData<"gateway-tcp">(id, { network: value.network });
97 } else if (name === "subdomain") {
98 store.updateNodeData<"gateway-tcp">(id, { subdomain: value.subdomain });
99 }
100 },
101 );
102 return () => sub.unsubscribe();
103 }, [id, data, form, store]);
104 const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
105 resolver: zodResolver(connectedToSchema),
106 mode: "onSubmit",
107 defaultValues: {
108 serviceId: data.selected?.serviceId,
109 portId: data.selected?.portId,
110 },
111 });
112 useEffect(() => {
113 connectedToForm.reset({
114 serviceId: data.selected?.serviceId,
115 portId: data.selected?.portId,
116 });
117 console.log(connectedToForm.getValues());
118 }, [id, connectedToForm, data]);
119 const nodes = useNodes<AppNode>();
120 const [selected, setSelected] = useState<AppNode | undefined>(undefined);
121 useEffect(() => {
122 if (data.selected?.serviceId == null) {
123 setSelected(undefined);
124 } else {
125 const serviceId = data.selected.serviceId;
126 setSelected(nodes.find((n) => n.id === serviceId));
127 }
128 }, [id, data, setSelected, nodes]);
129 const selectable = useMemo(() => {
130 console.log(selected);
131 return nodes.filter((n) => {
132 if (n.id === id) {
133 return false;
134 }
135 if (selected != null && selected.id === id) {
136 return true;
137 }
138 if ("ports" in n.data && (n.data.ports || []).length > 0) {
139 return true;
140 }
141 return false;
142 });
143 }, [id, nodes, selected]);
144 useEffect(() => {
145 const sub = connectedToForm.watch(
146 (
147 value: DeepPartial<z.infer<typeof connectedToSchema>>,
148 {
149 name,
150 type,
151 }: { name?: keyof z.infer<typeof connectedToSchema> | undefined; type?: EventType | undefined },
152 ) => {
153 if (type !== "change") {
154 return;
155 }
156 switch (name) {
157 case "serviceId":
158 if (!value.serviceId) {
159 break;
160 }
161 store.updateNodeData<"gateway-tcp">(id, {
162 selected: {
163 serviceId: value.serviceId,
164 },
165 });
166 break;
167 case "portId":
168 if (!value.portId) {
169 break;
170 }
171 store.updateNodeData<"gateway-tcp">(id, {
172 selected: {
173 serviceId: value.serviceId,
174 portId: value.portId,
175 },
176 });
177 break;
178 }
179 },
180 );
181 return () => sub.unsubscribe();
182 }, [id, connectedToForm, store]);
183 const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
184 const [portLabels, setPortLabels] = useState(new Map<string, string>());
185 useEffect(() => {
186 setNodeLabels(
187 new Map(
188 (data.exposed || []).map((e) => [e.serviceId, nodeLabel(nodes.find((n) => n.id === e.serviceId)!)]),
189 ),
190 );
191 setPortLabels(
192 new Map(
193 (data.exposed || []).map((e) => [
194 `${e.serviceId} - ${e.portId}`,
195 (nodes.find((n) => n.id === e.serviceId)!.data.ports || []).find((p) => p.id === e.portId)!.name,
196 ]),
197 ),
198 );
199 }, [nodes, data, setNodeLabels, setPortLabels]);
200 const onSubmit = useCallback(
201 (values: z.infer<typeof connectedToSchema>) => {
202 const edges = store.edges.filter((e) => e.target !== id);
203 const exp = (data.exposed || []).concat({
204 serviceId: values.serviceId,
205 portId: values.portId,
206 });
207 store.updateNodeData<"gateway-tcp">(id, {
208 exposed: exp,
209 selected: undefined,
210 });
211 store.setEdges(
212 edges.concat(
gio97efd722025-05-19 10:36:12 +0000213 exp.map((e): Edge => {
214 const sn = nodes.find((n) => n.id === e.serviceId);
215 if (sn == null) {
216 throw new Error(`Service ${e.serviceId} not found`);
217 }
218 if (sn.type === "app") {
219 return {
220 id: uuidv4(),
221 source: e.serviceId,
222 sourceHandle: "ports",
223 target: id,
224 targetHandle: "tcp",
225 };
226 } else {
227 return {
228 id: uuidv4(),
229 source: e.serviceId,
230 sourceHandle: "env_var",
231 target: id,
232 targetHandle: "tcp",
233 };
234 }
235 }),
giod0026612025-05-08 13:00:36 +0000236 ),
237 );
238 },
gio40370782025-05-19 11:04:52 +0000239 [id, data, store, nodes],
giod0026612025-05-08 13:00:36 +0000240 );
241 return (
242 <>
243 <Form {...form}>
244 <form className="space-y-2">
245 <FormField
246 control={form.control}
247 name="network"
248 render={({ field }) => (
249 <FormItem>
gio48fde052025-05-14 09:48:08 +0000250 <Select
251 onValueChange={field.onChange}
252 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000253 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000254 >
giod0026612025-05-08 13:00:36 +0000255 <FormControl>
256 <SelectTrigger>
257 <SelectValue placeholder="Network" />
258 </SelectTrigger>
259 </FormControl>
260 <SelectContent>
261 {env.networks.map((n) => (
262 <SelectItem
263 key={n.name}
264 value={n.domain}
265 >{`${n.name} - ${n.domain}`}</SelectItem>
266 ))}
267 </SelectContent>
268 </Select>
269 <FormMessage />
270 </FormItem>
271 )}
272 />
273 <FormField
274 control={form.control}
275 name="subdomain"
276 render={({ field }) => (
277 <FormItem>
278 <FormControl>
gio3ec94242025-05-16 12:46:57 +0000279 <Input placeholder="subdomain" {...field} disabled={data.readonly || disabled} />
giod0026612025-05-08 13:00:36 +0000280 </FormControl>
281 <FormMessage />
282 </FormItem>
283 )}
284 />
285 </form>
286 </Form>
287 Exposed Services
288 <ul>
289 {(data.exposed || []).map((e, i) => (
290 <li key={i}>
291 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
292 </li>
293 ))}
294 </ul>
295 <Form {...connectedToForm}>
296 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
297 <FormField
298 control={connectedToForm.control}
299 name="serviceId"
300 render={({ field }) => (
301 <FormItem>
gio48fde052025-05-14 09:48:08 +0000302 <Select
303 onValueChange={field.onChange}
304 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000305 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000306 >
giod0026612025-05-08 13:00:36 +0000307 <FormControl>
308 <SelectTrigger>
309 <SelectValue placeholder="Service" />
310 </SelectTrigger>
311 </FormControl>
312 <SelectContent>
313 {selectable.map((n) => (
314 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
315 ))}
316 </SelectContent>
317 </Select>
318 <FormMessage />
319 </FormItem>
320 )}
321 />
322 <FormField
323 control={connectedToForm.control}
324 name="portId"
325 render={({ field }) => (
326 <FormItem>
gio48fde052025-05-14 09:48:08 +0000327 <Select
328 onValueChange={field.onChange}
329 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000330 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000331 >
giod0026612025-05-08 13:00:36 +0000332 <FormControl>
333 <SelectTrigger>
334 <SelectValue placeholder="Port" />
335 </SelectTrigger>
336 </FormControl>
337 <SelectContent>
338 {selected &&
339 (selected.data.ports || []).map((p) => (
340 <SelectItem key={p.id} value={p.id}>
341 {p.name} - {p.value}
342 </SelectItem>
343 ))}
344 </SelectContent>
345 </Select>
346 <FormMessage />
347 </FormItem>
348 )}
349 />
gio3ec94242025-05-16 12:46:57 +0000350 <Button type="submit" disabled={disabled}>
351 Expose
352 </Button>
giod0026612025-05-08 13:00:36 +0000353 </form>
354 </Form>
355 </>
356 );
357}