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