blob: ada5ef9c88808940869822ccf777d7afe21f0645 [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>
235 <Select onValueChange={field.onChange} defaultValue={field.value}>
236 <FormControl>
237 <SelectTrigger>
238 <SelectValue placeholder="Network" />
239 </SelectTrigger>
240 </FormControl>
241 <SelectContent>
242 {env.networks.map((n) => (
243 <SelectItem
244 key={n.name}
245 value={n.domain}
246 >{`${n.name} - ${n.domain}`}</SelectItem>
247 ))}
248 </SelectContent>
249 </Select>
250 <FormMessage />
251 </FormItem>
252 )}
253 />
254 <FormField
255 control={form.control}
256 name="subdomain"
257 render={({ field }) => (
258 <FormItem>
259 <FormControl>
260 <Input placeholder="subdomain" className="border border-black" {...field} />
261 </FormControl>
262 <FormMessage />
263 </FormItem>
264 )}
265 />
266 </form>
267 </Form>
268 Exposed Services
269 <ul>
270 {(data.exposed || []).map((e, i) => (
271 <li key={i}>
272 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
273 </li>
274 ))}
275 </ul>
276 <Form {...connectedToForm}>
277 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
278 <FormField
279 control={connectedToForm.control}
280 name="serviceId"
281 render={({ field }) => (
282 <FormItem>
283 <Select onValueChange={field.onChange} defaultValue={field.value}>
284 <FormControl>
285 <SelectTrigger>
286 <SelectValue placeholder="Service" />
287 </SelectTrigger>
288 </FormControl>
289 <SelectContent>
290 {selectable.map((n) => (
291 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
292 ))}
293 </SelectContent>
294 </Select>
295 <FormMessage />
296 </FormItem>
297 )}
298 />
299 <FormField
300 control={connectedToForm.control}
301 name="portId"
302 render={({ field }) => (
303 <FormItem>
304 <Select onValueChange={field.onChange} defaultValue={field.value}>
305 <FormControl>
306 <SelectTrigger>
307 <SelectValue placeholder="Port" />
308 </SelectTrigger>
309 </FormControl>
310 <SelectContent>
311 {selected &&
312 (selected.data.ports || []).map((p) => (
313 <SelectItem key={p.id} value={p.id}>
314 {p.name} - {p.value}
315 </SelectItem>
316 ))}
317 </SelectContent>
318 </Select>
319 <FormMessage />
320 </FormItem>
321 )}
322 />
323 <Button type="submit">Expose</Button>
324 </form>
325 </Form>
326 </>
327 );
328}