blob: 919bf839b1f7f00a3dfc0b2edaca819d75c166f7 [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";
gio3fb133d2025-06-13 07:20:24 +000013import { NodeDetailsProps } from "@/lib/types";
gio5f2f1002025-03-20 18:38:48 +040014
15const schema = z.object({
giod0026612025-05-08 13:00:36 +000016 network: z.string().min(1, "reqired"),
17 subdomain: z.string().min(1, "required"),
gio5f2f1002025-03-20 18:38:48 +040018});
19
20const connectedToSchema = z.object({
giod0026612025-05-08 13:00:36 +000021 serviceId: z.string(),
22 portId: z.string(),
gio5f2f1002025-03-20 18:38:48 +040023});
24
25export function NodeGatewayTCP(node: GatewayTCPNode) {
giod0026612025-05-08 13:00:36 +000026 const { id, selected } = node;
27 const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
28 const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
29 return (
30 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
31 {nodeLabel(node)}
32 <Handle
33 type={"source"}
34 id="subdomain"
35 position={Position.Top}
36 isConnectable={isConnectableNetwork}
37 isConnectableStart={isConnectableNetwork}
38 isConnectableEnd={isConnectableNetwork}
39 />
40 <Handle
41 type={"target"}
42 id="tcp"
43 position={Position.Bottom}
44 isConnectable={isConnectable}
45 isConnectableStart={isConnectable}
46 isConnectableEnd={isConnectable}
47 />
48 </NodeRect>
49 );
gio5f2f1002025-03-20 18:38:48 +040050}
51
gio3fb133d2025-06-13 07:20:24 +000052export function NodeGatewayTCPDetails({ node, disabled }: NodeDetailsProps<GatewayTCPNode>) {
gio08acd3a2025-06-12 12:15:30 +000053 const { id, data } = node;
giod0026612025-05-08 13:00:36 +000054 const store = useStateStore();
55 const env = useEnv();
56 const form = useForm<z.infer<typeof schema>>({
57 resolver: zodResolver(schema),
58 mode: "onChange",
59 defaultValues: {
60 network: data.network,
61 subdomain: data.subdomain,
62 },
63 });
64 useEffect(() => {
65 const sub = form.watch(
66 (
67 value: DeepPartial<z.infer<typeof schema>>,
68 { name }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
69 ) => {
70 if (name === "network") {
71 let edges = store.edges;
72 if (data.network !== undefined) {
73 edges = edges.filter((e) => {
74 console.log(e);
75 if (
76 e.source === id &&
77 e.sourceHandle === "subdomain" &&
78 e.target === data.network &&
79 e.targetHandle === "subdomain"
80 ) {
81 return false;
82 } else {
83 return true;
84 }
85 });
86 }
87 if (value.network !== undefined) {
88 edges = edges.concat({
89 id: uuidv4(),
90 source: id,
91 sourceHandle: "subdomain",
92 target: value.network,
93 targetHandle: "subdomain",
94 });
95 }
96 store.setEdges(edges);
97 store.updateNodeData<"gateway-tcp">(id, { network: value.network });
98 } else if (name === "subdomain") {
99 store.updateNodeData<"gateway-tcp">(id, { subdomain: value.subdomain });
100 }
101 },
102 );
103 return () => sub.unsubscribe();
104 }, [id, data, form, store]);
105 const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
106 resolver: zodResolver(connectedToSchema),
107 mode: "onSubmit",
108 defaultValues: {
109 serviceId: data.selected?.serviceId,
110 portId: data.selected?.portId,
111 },
112 });
113 useEffect(() => {
114 connectedToForm.reset({
115 serviceId: data.selected?.serviceId,
116 portId: data.selected?.portId,
117 });
118 console.log(connectedToForm.getValues());
119 }, [id, connectedToForm, data]);
120 const nodes = useNodes<AppNode>();
121 const [selected, setSelected] = useState<AppNode | undefined>(undefined);
122 useEffect(() => {
123 if (data.selected?.serviceId == null) {
124 setSelected(undefined);
125 } else {
126 const serviceId = data.selected.serviceId;
127 setSelected(nodes.find((n) => n.id === serviceId));
128 }
129 }, [id, data, setSelected, nodes]);
130 const selectable = useMemo(() => {
131 console.log(selected);
132 return nodes.filter((n) => {
133 if (n.id === id) {
134 return false;
135 }
136 if (selected != null && selected.id === id) {
137 return true;
138 }
139 if ("ports" in n.data && (n.data.ports || []).length > 0) {
140 return true;
141 }
142 return false;
143 });
144 }, [id, nodes, selected]);
145 useEffect(() => {
146 const sub = connectedToForm.watch(
147 (
148 value: DeepPartial<z.infer<typeof connectedToSchema>>,
149 {
150 name,
151 type,
152 }: { name?: keyof z.infer<typeof connectedToSchema> | undefined; type?: EventType | undefined },
153 ) => {
154 if (type !== "change") {
155 return;
156 }
157 switch (name) {
158 case "serviceId":
159 if (!value.serviceId) {
160 break;
161 }
162 store.updateNodeData<"gateway-tcp">(id, {
163 selected: {
164 serviceId: value.serviceId,
165 },
166 });
167 break;
168 case "portId":
169 if (!value.portId) {
170 break;
171 }
172 store.updateNodeData<"gateway-tcp">(id, {
173 selected: {
174 serviceId: value.serviceId,
175 portId: value.portId,
176 },
177 });
178 break;
179 }
180 },
181 );
182 return () => sub.unsubscribe();
183 }, [id, connectedToForm, store]);
184 const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
185 const [portLabels, setPortLabels] = useState(new Map<string, string>());
186 useEffect(() => {
187 setNodeLabels(
188 new Map(
189 (data.exposed || []).map((e) => [e.serviceId, nodeLabel(nodes.find((n) => n.id === e.serviceId)!)]),
190 ),
191 );
192 setPortLabels(
193 new Map(
194 (data.exposed || []).map((e) => [
195 `${e.serviceId} - ${e.portId}`,
196 (nodes.find((n) => n.id === e.serviceId)!.data.ports || []).find((p) => p.id === e.portId)!.name,
197 ]),
198 ),
199 );
200 }, [nodes, data, setNodeLabels, setPortLabels]);
201 const onSubmit = useCallback(
202 (values: z.infer<typeof connectedToSchema>) => {
203 const edges = store.edges.filter((e) => e.target !== id);
204 const exp = (data.exposed || []).concat({
205 serviceId: values.serviceId,
206 portId: values.portId,
207 });
208 store.updateNodeData<"gateway-tcp">(id, {
209 exposed: exp,
210 selected: undefined,
211 });
212 store.setEdges(
213 edges.concat(
gio97efd722025-05-19 10:36:12 +0000214 exp.map((e): Edge => {
215 const sn = nodes.find((n) => n.id === e.serviceId);
216 if (sn == null) {
217 throw new Error(`Service ${e.serviceId} not found`);
218 }
219 if (sn.type === "app") {
220 return {
221 id: uuidv4(),
222 source: e.serviceId,
223 sourceHandle: "ports",
224 target: id,
225 targetHandle: "tcp",
226 };
227 } else {
228 return {
229 id: uuidv4(),
230 source: e.serviceId,
231 sourceHandle: "env_var",
232 target: id,
233 targetHandle: "tcp",
234 };
235 }
236 }),
giod0026612025-05-08 13:00:36 +0000237 ),
238 );
239 },
gio40370782025-05-19 11:04:52 +0000240 [id, data, store, nodes],
giod0026612025-05-08 13:00:36 +0000241 );
242 return (
243 <>
244 <Form {...form}>
245 <form className="space-y-2">
246 <FormField
247 control={form.control}
248 name="network"
249 render={({ field }) => (
250 <FormItem>
gio48fde052025-05-14 09:48:08 +0000251 <Select
252 onValueChange={field.onChange}
253 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000254 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000255 >
giod0026612025-05-08 13:00:36 +0000256 <FormControl>
257 <SelectTrigger>
258 <SelectValue placeholder="Network" />
259 </SelectTrigger>
260 </FormControl>
261 <SelectContent>
262 {env.networks.map((n) => (
263 <SelectItem
264 key={n.name}
265 value={n.domain}
266 >{`${n.name} - ${n.domain}`}</SelectItem>
267 ))}
268 </SelectContent>
269 </Select>
270 <FormMessage />
271 </FormItem>
272 )}
273 />
274 <FormField
275 control={form.control}
276 name="subdomain"
277 render={({ field }) => (
278 <FormItem>
279 <FormControl>
gio3ec94242025-05-16 12:46:57 +0000280 <Input placeholder="subdomain" {...field} disabled={data.readonly || disabled} />
giod0026612025-05-08 13:00:36 +0000281 </FormControl>
282 <FormMessage />
283 </FormItem>
284 )}
285 />
286 </form>
287 </Form>
288 Exposed Services
289 <ul>
290 {(data.exposed || []).map((e, i) => (
291 <li key={i}>
292 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
293 </li>
294 ))}
295 </ul>
296 <Form {...connectedToForm}>
297 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
298 <FormField
299 control={connectedToForm.control}
300 name="serviceId"
301 render={({ field }) => (
302 <FormItem>
gio48fde052025-05-14 09:48:08 +0000303 <Select
304 onValueChange={field.onChange}
305 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000306 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000307 >
giod0026612025-05-08 13:00:36 +0000308 <FormControl>
309 <SelectTrigger>
310 <SelectValue placeholder="Service" />
311 </SelectTrigger>
312 </FormControl>
313 <SelectContent>
314 {selectable.map((n) => (
315 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
316 ))}
317 </SelectContent>
318 </Select>
319 <FormMessage />
320 </FormItem>
321 )}
322 />
323 <FormField
324 control={connectedToForm.control}
325 name="portId"
326 render={({ field }) => (
327 <FormItem>
gio48fde052025-05-14 09:48:08 +0000328 <Select
329 onValueChange={field.onChange}
330 defaultValue={field.value}
gio3ec94242025-05-16 12:46:57 +0000331 disabled={data.readonly || disabled}
gio48fde052025-05-14 09:48:08 +0000332 >
giod0026612025-05-08 13:00:36 +0000333 <FormControl>
334 <SelectTrigger>
335 <SelectValue placeholder="Port" />
336 </SelectTrigger>
337 </FormControl>
338 <SelectContent>
339 {selected &&
340 (selected.data.ports || []).map((p) => (
341 <SelectItem key={p.id} value={p.id}>
342 {p.name} - {p.value}
343 </SelectItem>
344 ))}
345 </SelectContent>
346 </Select>
347 <FormMessage />
348 </FormItem>
349 )}
350 />
gio3ec94242025-05-16 12:46:57 +0000351 <Button type="submit" disabled={disabled}>
352 Expose
353 </Button>
giod0026612025-05-08 13:00:36 +0000354 </form>
355 </Form>
356 </>
357 );
358}