blob: 9bc5f9233a942434394d5d0d8914ea5ee67381fb [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { v4 as uuidv4 } from "uuid";
2import { NodeRect } from './node-rect';
3import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable } from '@/lib/state';
4import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
5import { z } from "zod";
6import { DeepPartial, EventType, useForm } from 'react-hook-form';
7import { zodResolver } from '@hookform/resolvers/zod';
8import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
9import { Input } from './ui/input';
10import { Button } from './ui/button';
11import { Handle, Position } from "@xyflow/react";
12import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
13import { EditIcon } from "lucide-react";
14import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
15
16export function NodeApp(node: ServiceNode) {
17 const { id, selected } = node;
18 const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
19 const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
20 return (
21 <NodeRect id={id} selected={selected} type={node.type}>
22 <div style={{ padding: '10px 20px' }}>
23 {nodeLabel(node)}
24 <Handle
25 id="repository"
26 type={"target"}
27 position={Position.Left}
28 isConnectableStart={isConnectableRepository}
29 isConnectableEnd={isConnectableRepository}
30 isConnectable={isConnectableRepository}
31 />
32 <Handle
33 id="ports"
34 type={"source"}
35 position={Position.Top}
36 isConnectableStart={isConnectablePorts}
37 isConnectableEnd={isConnectablePorts}
38 isConnectable={isConnectablePorts}
39 />
40 <Handle
41 id="env_var"
42 type={"target"}
43 position={Position.Bottom}
44 isConnectableStart={true}
45 isConnectableEnd={true}
46 isConnectable={true}
47 />
48 </div>
49 </NodeRect>
50 );
51}
52
53const schema = z.object({
54 name: z.string().min(1, "requried"),
55 type: z.enum(ServiceTypes),
56});
57
58const portSchema = z.object({
59 name: z.string().min(1, "required"),
60 value: z.coerce.number().gt(0, "can not be negative"),
61});
62
63export function NodeAppDetails({ id, data }: ServiceNode) {
64 const store = useStateStore();
65 const form = useForm<z.infer<typeof schema>>({
66 resolver: zodResolver(schema),
67 mode: "onChange",
68 defaultValues: {
69 name: data.label,
70 type: data.type,
71 }
72 });
73 const portForm = useForm<z.infer<typeof portSchema>>({
74 resolver: zodResolver(portSchema),
75 mode: "onSubmit",
76 defaultValues: {
77 name: "",
78 value: 0,
79 }
80 });
81 const onSubmit = useCallback((values: z.infer<typeof portSchema>) => {
82 store.updateNodeData<"app">(id, {
83 ports: (data.ports || []).concat({
84 id: uuidv4(),
85 name: values.name,
86 value: values.value,
gio355883e2025-04-23 14:10:51 +000087 }),
88 envVars: (data.envVars || []).concat({
89 id: uuidv4(),
90 source: null,
91 name: `DODO_PORT_${values.name.toUpperCase()}`,
92 }),
gio5f2f1002025-03-20 18:38:48 +040093 });
94 portForm.reset();
95 }, [data, portForm]);
96 useEffect(() => {
97 const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
98 console.log({ name, type });
99 if (type !== "change") {
100 return;
101 }
102 switch (name) {
103 case "name":
104 if (!value.name) {
105 break;
106 }
107 store.updateNodeData<"app">(id, {
108 label: value.name,
109 });
110 break;
111 case "type":
112 if (!value.type) {
113 break;
114 }
115 store.updateNodeData<"app">(id, {
116 type: value.type,
117 })
118 break;
119 }
120 });
121 return () => sub.unsubscribe();
122 }, [form, store]);
123 const focus = useCallback((field: any, name: string) => {
124 return (e: HTMLElement | null) => {
125 field.ref(e);
126 if (e != null && name === data.activeField) {
127 console.log(e);
128 e.focus();
129 store.updateNodeData(id, {
130 activeField: undefined,
131 });
132 }
133 }
134 }, [data, store]);
135 const [typeProps, setTypeProps] = useState({});
136 useEffect(() => {
137 if (data.activeField === "type") {
138 setTypeProps({
139 open: true,
140 onOpenChange: () => store.updateNodeData(id, { activeField: undefined }),
141 });
142 } else {
143 setTypeProps({});
144 }
145 }, [store, data, setTypeProps]);
146 const editAlias = useCallback((e: BoundEnvVar) => {
147 return () => {
148 store.updateNodeData(id, {
149 ...data,
150 envVars: data.envVars!.map((o) => {
151 if (o.id !== e.id) {
152 return o;
153 } else return {
154 ...o,
155 isEditting: true,
156 }
157 }),
158 });
159 };
160 }, [id, data, store]);
161 const saveAlias = (e: BoundEnvVar, value: string, store: AppState) => {
162 store.updateNodeData(id, {
163 ...data,
164 envVars: data.envVars!.map((o) => {
165 if (o.id !== e.id) {
166 return o;
167 }
168 if (value) {
169 return {
170 ...o,
171 isEditting: false,
172 alias: value.toUpperCase(),
173 }
174 }
175 console.log(o);
176 if ("alias" in o) {
177 const { alias: tmp, ...rest } = o;
178 console.log(rest);
179 return {
180 ...rest,
181 isEditting: false,
182 };
183 }
184 return {
185 ...o,
186 isEditting: false,
187 };
188 }),
189 });
190 };
191 const saveAliasOnEnter = useCallback((e: BoundEnvVar) => {
192 return (event: KeyboardEvent<HTMLInputElement>) => {
193 if (event.key === "Enter") {
194 event.preventDefault();
195 saveAlias(e, event.currentTarget.value, store);
196 }
197 }
198 }, [id, data, store]);
199 const saveAliasOnBlur = useCallback((e: BoundEnvVar) => {
200 return (event: FocusEvent<HTMLInputElement>) => {
201 saveAlias(e, event.currentTarget.value, store);
202 }
203 }, [id, data, store]);
204 return (
205 <>
206 <Form {...form}>
207 <form>
208 <FormField
209 control={form.control}
210 name="name"
211 render={({ field }) => (
212 <FormItem>
213 <FormControl>
214 <Input placeholder="name" className="border border-black" {...field} ref={focus(field, "name")} />
215 </FormControl>
216 <FormMessage />
217 </FormItem>
218 )}
219 />
220 <FormField
221 control={form.control}
222 name="type"
223 render={({ field }) => (
224 <FormItem>
225 <Select onValueChange={field.onChange} defaultValue={field.value} {...typeProps}>
226 <FormControl>
227 <SelectTrigger>
228 <SelectValue placeholder="Runtime" />
229 </SelectTrigger>
230 </FormControl>
231 <SelectContent>
232 {ServiceTypes.map((t) => (
233 <SelectItem key={t} value={t}>{t}</SelectItem>
234 ))}
235 </SelectContent>
236 </Select>
237 <FormMessage />
238 </FormItem>
239 )}
240 />
241 </form>
242 </Form>
243 Ports
244 <ul>
245 {data && data.ports && data.ports.map((p) => (<li key={p.id}>{p.name} - {p.value}</li>))}
246 </ul>
247 <Form {...portForm}>
248 <form className="flex flex-row space-x-1" onSubmit={portForm.handleSubmit(onSubmit)}>
249 <FormField
250 control={portForm.control}
251 name="name"
252 render={({ field }) => (
253 <FormItem>
254 <FormControl>
255 <Input placeholder="name" className="border border-black" {...field} />
256 </FormControl>
257 <FormMessage />
258 </FormItem>
259 )}
260 />
261 <FormField
262 control={portForm.control}
263 name="value"
264 render={({ field }) => (
265 <FormItem>
266 <FormControl>
267 <Input placeholder="value" className="border border-black" {...field} />
268 </FormControl>
269 <FormMessage />
270 </FormItem>
271 )}
272 />
273 <Button type="submit">Add Port</Button>
274 </form>
275 </Form>
276 Env Vars
277 <ul>
278 {data && data.envVars && data.envVars.map((v) => {
279 if ("name" in v) {
280 const value = "alias" in v ? v.alias : v.name;
281 if (v.isEditting) {
282 return (<li key={v.id}><Input type="text" className="border border-black" defaultValue={value} onKeyUp={saveAliasOnEnter(v)} onBlur={saveAliasOnBlur(v)} autoFocus={true} /></li>);
283 }
284 return (
285 <li key={v.id} onClick={editAlias(v)}>
286 <TooltipProvider>
287 <Tooltip>
288 <TooltipTrigger>
289 <Button size={"icon"} variant={"ghost"}><EditIcon /></Button>
290 {value}
291 </TooltipTrigger>
292 <TooltipContent>
293 {v.name}
294 </TooltipContent>
295 </Tooltip>
296 </TooltipProvider>
297 </li>
298 );
299 }
300 })}
301 </ul>
302 </>);
303}