blob: 358850251b0a6f0fe5e0914a6eb3abf51ad0f85c [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { v4 as uuidv4 } from "uuid";
2import { 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';
6import { z } from "zod";
7import { zodResolver } from "@hookform/resolvers/zod";
8import { 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';
12import { Button } from "./ui/button";
13
14const schema = z.object({
15 network: z.string().min(1, "reqired"),
16 subdomain: z.string().min(1, "required"),
17});
18
19const connectedToSchema = z.object({
20 serviceId: z.string(),
21 portId: z.string(),
22});
23
24export function NodeGatewayTCP(node: GatewayTCPNode) {
25 const { id, selected } = node;
gioaba9a962025-04-25 14:19:40 +000026 const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
gio5f2f1002025-03-20 18:38:48 +040027 const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
28 return (
gio1dc800a2025-04-24 17:15:43 +000029 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
gio5f2f1002025-03-20 18:38:48 +040030 {nodeLabel(node)}
31 <Handle
gioaba9a962025-04-25 14:19:40 +000032 type={"source"}
33 id="subdomain"
34 position={Position.Top}
35 isConnectable={isConnectableNetwork}
36 isConnectableStart={isConnectableNetwork}
37 isConnectableEnd={isConnectableNetwork}
38 />
39 <Handle
gio5f2f1002025-03-20 18:38:48 +040040 type={"target"}
gioaba9a962025-04-25 14:19:40 +000041 id="tcp"
gio5f2f1002025-03-20 18:38:48 +040042 position={Position.Bottom}
43 isConnectable={isConnectable}
gioaba9a962025-04-25 14:19:40 +000044 isConnectableStart={isConnectable}
45 isConnectableEnd={isConnectable}
gio5f2f1002025-03-20 18:38:48 +040046 />
47 </NodeRect>
48 );
49}
50
51export function NodeGatewayTCPDetails({ id, data }: GatewayTCPNode) {
52 const store = useStateStore();
53 const env = useEnv();
54 const form = useForm<z.infer<typeof schema>>({
55 resolver: zodResolver(schema),
56 mode: "onChange",
57 defaultValues: {
gioaba9a962025-04-25 14:19:40 +000058 network: data.network,
59 subdomain: data.subdomain,
gio5f2f1002025-03-20 18:38:48 +040060 },
61 });
62 useEffect(() => {
63 const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
64 if (name === "network") {
gioaba9a962025-04-25 14:19:40 +000065 let edges = store.edges;
66 if (data.network !== undefined) {
67 edges = edges.filter((e) => {
68 console.log(e);
69 if (e.source === id && e.sourceHandle === "subdomain" && e.target === data.network && e.targetHandle === "subdomain") {
70 return false;
71 } else {
72 return true;
73 }
74 });
gio5f2f1002025-03-20 18:38:48 +040075 }
gioaba9a962025-04-25 14:19:40 +000076 if (value.network !== undefined) {
77 edges = edges.concat({
78 id: uuidv4(),
79 source: id,
80 sourceHandle: "subdomain",
81 target: value.network,
82 targetHandle: "subdomain",
83 });
84 }
85 store.setEdges(edges);
86 store.updateNodeData<"gateway-tcp">(id, { network: value.network });
gio5f2f1002025-03-20 18:38:48 +040087 } else if (name === "subdomain") {
88 store.updateNodeData<"gateway-tcp">(id, { subdomain: value.subdomain });
89 }
90 });
91 return () => sub.unsubscribe();
gioaba9a962025-04-25 14:19:40 +000092 }, [id, data, form, store]);
gio5f2f1002025-03-20 18:38:48 +040093 const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
94 resolver: zodResolver(connectedToSchema),
95 mode: "onSubmit",
96 defaultValues: {
97 serviceId: data.selected?.serviceId,
98 portId: data.selected?.portId,
99 },
100 });
101 useEffect(() => {
102 connectedToForm.reset({
103 serviceId: data.selected?.serviceId,
104 portId: data.selected?.portId,
105 });
106 console.log(connectedToForm.getValues());
107 }, [connectedToForm, data]);
108 const nodes = useNodes<AppNode>();
109 const [selected, setSelected] = useState<AppNode | undefined>(undefined);
110 useEffect(() => {
111 if (data.selected?.serviceId == null) {
112 setSelected(undefined);
113 } else {
114 const serviceId = data.selected.serviceId;
115 setSelected(nodes.find((n) => n.id === serviceId));
116 }
117 }, [data, setSelected]);
118 const selectable = useMemo(() => {
119 console.log(selected);
120 return nodes.filter((n) => {
121 if (n.id === id) {
122 return false;
123 }
124 if (selected != null && selected.id === id) {
125 return true;
126 }
127 if ("ports" in n.data && (n.data.ports || []).length > 0) {
128 return true;
129 }
130 return false;
131 })
132 }, [nodes, selected]);
133 useEffect(() => {
134 const sub = connectedToForm.watch((value: DeepPartial<z.infer<typeof connectedToSchema>>, { name, type }: { name?: keyof z.infer<typeof connectedToSchema> | undefined, type?: EventType | undefined }) => {
135 if (type !== "change") {
136 return;
137 }
138 switch (name) {
139 case "serviceId":
140 if (!value.serviceId) {
141 break;
142 }
143 store.updateNodeData<"gateway-tcp">(id, {
144 selected: {
145 serviceId: value.serviceId,
146 },
147 });
148 break;
149 case "portId":
150 if (!value.portId) {
151 break;
152 }
153 store.updateNodeData<"gateway-tcp">(id, {
154 selected: {
155 serviceId: value.serviceId,
156 portId: value.portId,
157 },
158 });
159 break;
160 }
161 });
162 return () => sub.unsubscribe();
163 }, [connectedToForm, store]);
164 const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
165 const [portLabels, setPortLabels] = useState(new Map<string, string>());
166 useEffect(() => {
167 setNodeLabels(new Map((data.exposed || []).map((e) => [e.serviceId, nodeLabel(nodes.find((n) => n.id === e.serviceId)!)])));
168 setPortLabels(new Map((data.exposed || []).map((e) => [`${e.serviceId} - ${e.portId}`, (nodes.find((n) => n.id === e.serviceId)!.data.ports || []).find((p) => p.id === e.portId)!.name])));
169 }, [nodes, data, setNodeLabels, setPortLabels]);
170 const onSubmit = useCallback((values: z.infer<typeof connectedToSchema>) => {
gioaba9a962025-04-25 14:19:40 +0000171 const edges = store.edges.filter((e) => e.target !== id);
gio5f2f1002025-03-20 18:38:48 +0400172 const exp = (data.exposed || []).concat({
173 serviceId: values.serviceId,
174 portId: values.portId,
175 });
176 store.updateNodeData<"gateway-tcp">(id, {
177 exposed: exp,
178 selected: undefined,
179 });
gioaba9a962025-04-25 14:19:40 +0000180 store.setEdges(edges.concat(exp.map((e): Edge => ({
gio5f2f1002025-03-20 18:38:48 +0400181 id: uuidv4(),
182 source: e.serviceId,
183 sourceHandle: "ports",
184 target: id,
185 targetHandle: "tcp",
186 }))));
187 }, [id, data, connectedToForm, store, setNodeLabels, setPortLabels]);
188 return (
189 <>
190 <Form {...form}>
191 <form className="space-y-2">
gioaba9a962025-04-25 14:19:40 +0000192 <FormField
gio5f2f1002025-03-20 18:38:48 +0400193 control={form.control}
194 name="network"
195 render={({ field }) => (
196 <FormItem>
197 <Select onValueChange={field.onChange} defaultValue={field.value}>
198 <FormControl>
199 <SelectTrigger>
200 <SelectValue placeholder="Network" />
201 </SelectTrigger>
202 </FormControl>
203 <SelectContent>
204 {env.networks.map((n) => (
205 <SelectItem key={n.name} value={n.domain}>{`${n.name} - ${n.domain}`}</SelectItem>
206 ))}
207 </SelectContent>
208 </Select>
209 <FormMessage />
210 </FormItem>
211 )}
212 />
gioaba9a962025-04-25 14:19:40 +0000213 <FormField
gio5f2f1002025-03-20 18:38:48 +0400214 control={form.control}
215 name="subdomain"
216 render={({ field }) => (
217 <FormItem>
218 <FormControl>
219 <Input placeholder="subdomain" className="border border-black" {...field} />
220 </FormControl>
221 <FormMessage />
222 </FormItem>
223 )}
224 />
225 </form>
226 </Form>
227 Exposed Services
228 <ul>
229 {(data.exposed || []).map((e, i) => (
230 <li key={i}>
231 {nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
232 </li>
233 ))}
234 </ul>
235 <Form {...connectedToForm}>
236 <form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
gioaba9a962025-04-25 14:19:40 +0000237 <FormField
gio5f2f1002025-03-20 18:38:48 +0400238 control={connectedToForm.control}
239 name="serviceId"
240 render={({ field }) => (
241 <FormItem>
242 <Select onValueChange={field.onChange} defaultValue={field.value}>
243 <FormControl>
244 <SelectTrigger>
245 <SelectValue placeholder="Service" />
246 </SelectTrigger>
247 </FormControl>
248 <SelectContent>
249 {selectable.map((n) => (
250 <SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
251 ))}
252 </SelectContent>
253 </Select>
254 <FormMessage />
255 </FormItem>
256 )}
257 />
gioaba9a962025-04-25 14:19:40 +0000258 <FormField
gio5f2f1002025-03-20 18:38:48 +0400259 control={connectedToForm.control}
260 name="portId"
261 render={({ field }) => (
262 <FormItem>
263 <Select onValueChange={field.onChange} defaultValue={field.value}>
264 <FormControl>
265 <SelectTrigger>
266 <SelectValue placeholder="Port" />
267 </SelectTrigger>
268 </FormControl>
269 <SelectContent>
270 {selected && (selected.data.ports || []).map((p) => (
271 <SelectItem key={p.id} value={p.id}>{p.name} - {p.value}</SelectItem>
272 ))}
273 </SelectContent>
274 </Select>
275 <FormMessage />
gioaba9a962025-04-25 14:19:40 +0000276 </FormItem>
gio5f2f1002025-03-20 18:38:48 +0400277 )}
278 />
279 <Button type="submit">Expose</Button>
280 </form>
281 </Form>
282 </>
283 );
284}