blob: 86fa493eb15d790fe4687da6dc92457644f165e9 [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
51export function NodeGatewayTCPDetails({ id, data }: GatewayTCPNode) {
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(
212 exp.map(
213 (e): Edge => ({
214 id: uuidv4(),
215 source: e.serviceId,
216 sourceHandle: "ports",
217 target: id,
218 targetHandle: "tcp",
219 }),
220 ),
221 ),
222 );
223 },
224 [id, data, store],
225 );
226 return (
227 <>
228 <Form {...form}>
229 <form className="space-y-2">
230 <FormField
231 control={form.control}
232 name="network"
233 render={({ field }) => (
234 <FormItem>
gio48fde052025-05-14 09:48:08 +0000235 <Select
236 onValueChange={field.onChange}
237 defaultValue={field.value}
238 disabled={data.readonly}
239 >
giod0026612025-05-08 13:00:36 +0000240 <FormControl>
241 <SelectTrigger>
242 <SelectValue placeholder="Network" />
243 </SelectTrigger>
244 </FormControl>
245 <SelectContent>
246 {env.networks.map((n) => (
247 <SelectItem
248 key={n.name}
249 value={n.domain}
250 >{`${n.name} - ${n.domain}`}</SelectItem>
251 ))}
252 </SelectContent>
253 </Select>
254 <FormMessage />
255 </FormItem>
256 )}
257 />
258 <FormField
259 control={form.control}
260 name="subdomain"
261 render={({ field }) => (
262 <FormItem>
263 <FormControl>
gio48fde052025-05-14 09:48:08 +0000264 <Input placeholder="subdomain" {...field} disabled={data.readonly} />
giod0026612025-05-08 13:00:36 +0000265 </FormControl>
266 <FormMessage />
267 </FormItem>
268 )}
269 />
270 </form>
271 </Form>
272 Exposed Services
273 <ul>
274 {(data.exposed || []).map((e, i) => (
275 <li key={i}>
276 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
277 </li>
278 ))}
279 </ul>
280 <Form {...connectedToForm}>
281 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
282 <FormField
283 control={connectedToForm.control}
284 name="serviceId"
285 render={({ field }) => (
286 <FormItem>
gio48fde052025-05-14 09:48:08 +0000287 <Select
288 onValueChange={field.onChange}
289 defaultValue={field.value}
290 disabled={data.readonly}
291 >
giod0026612025-05-08 13:00:36 +0000292 <FormControl>
293 <SelectTrigger>
294 <SelectValue placeholder="Service" />
295 </SelectTrigger>
296 </FormControl>
297 <SelectContent>
298 {selectable.map((n) => (
299 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
300 ))}
301 </SelectContent>
302 </Select>
303 <FormMessage />
304 </FormItem>
305 )}
306 />
307 <FormField
308 control={connectedToForm.control}
309 name="portId"
310 render={({ field }) => (
311 <FormItem>
gio48fde052025-05-14 09:48:08 +0000312 <Select
313 onValueChange={field.onChange}
314 defaultValue={field.value}
315 disabled={data.readonly}
316 >
giod0026612025-05-08 13:00:36 +0000317 <FormControl>
318 <SelectTrigger>
319 <SelectValue placeholder="Port" />
320 </SelectTrigger>
321 </FormControl>
322 <SelectContent>
323 {selected &&
324 (selected.data.ports || []).map((p) => (
325 <SelectItem key={p.id} value={p.id}>
326 {p.name} - {p.value}
327 </SelectItem>
328 ))}
329 </SelectContent>
330 </Select>
331 <FormMessage />
332 </FormItem>
333 )}
334 />
335 <Button type="submit">Expose</Button>
336 </form>
337 </Form>
338 </>
339 );
340}