blob: 74f0e755c17bb4f15f475d7836d73c276f5c4e69 [file] [log] [blame]
import { v4 as uuidv4 } from "uuid";
import { NodeRect } from './node-rect';
import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable } from '@/lib/state';
import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { z } from "zod";
import { DeepPartial, EventType, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
import { Input } from './ui/input';
import { Button } from './ui/button';
import { Handle, Position } from "@xyflow/react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { EditIcon } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
export function NodeApp(node: ServiceNode) {
const { id, selected } = node;
const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
return (
<NodeRect id={id} selected={selected} type={node.type}>
<div style={{ padding: '10px 20px' }}>
{nodeLabel(node)}
<Handle
id="repository"
type={"target"}
position={Position.Left}
isConnectableStart={isConnectableRepository}
isConnectableEnd={isConnectableRepository}
isConnectable={isConnectableRepository}
/>
<Handle
id="ports"
type={"source"}
position={Position.Top}
isConnectableStart={isConnectablePorts}
isConnectableEnd={isConnectablePorts}
isConnectable={isConnectablePorts}
/>
<Handle
id="env_var"
type={"target"}
position={Position.Bottom}
isConnectableStart={true}
isConnectableEnd={true}
isConnectable={true}
/>
</div>
</NodeRect>
);
}
const schema = z.object({
name: z.string().min(1, "requried"),
type: z.enum(ServiceTypes),
});
const portSchema = z.object({
name: z.string().min(1, "required"),
value: z.coerce.number().gt(0, "can not be negative"),
});
export function NodeAppDetails({ id, data }: ServiceNode) {
const store = useStateStore();
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
mode: "onChange",
defaultValues: {
name: data.label,
type: data.type,
}
});
const portForm = useForm<z.infer<typeof portSchema>>({
resolver: zodResolver(portSchema),
mode: "onSubmit",
defaultValues: {
name: "",
value: 0,
}
});
const onSubmit = useCallback((values: z.infer<typeof portSchema>) => {
store.updateNodeData<"app">(id, {
ports: (data.ports || []).concat({
id: uuidv4(),
name: values.name,
value: values.value,
})
});
portForm.reset();
}, [data, portForm]);
useEffect(() => {
const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
console.log({ name, type });
if (type !== "change") {
return;
}
switch (name) {
case "name":
if (!value.name) {
break;
}
store.updateNodeData<"app">(id, {
label: value.name,
});
break;
case "type":
if (!value.type) {
break;
}
store.updateNodeData<"app">(id, {
type: value.type,
})
break;
}
});
return () => sub.unsubscribe();
}, [form, store]);
const focus = useCallback((field: any, name: string) => {
return (e: HTMLElement | null) => {
field.ref(e);
if (e != null && name === data.activeField) {
console.log(e);
e.focus();
store.updateNodeData(id, {
activeField: undefined,
});
}
}
}, [data, store]);
const [typeProps, setTypeProps] = useState({});
useEffect(() => {
if (data.activeField === "type") {
setTypeProps({
open: true,
onOpenChange: () => store.updateNodeData(id, { activeField: undefined }),
});
} else {
setTypeProps({});
}
}, [store, data, setTypeProps]);
const editAlias = useCallback((e: BoundEnvVar) => {
return () => {
store.updateNodeData(id, {
...data,
envVars: data.envVars!.map((o) => {
if (o.id !== e.id) {
return o;
} else return {
...o,
isEditting: true,
}
}),
});
};
}, [id, data, store]);
const saveAlias = (e: BoundEnvVar, value: string, store: AppState) => {
store.updateNodeData(id, {
...data,
envVars: data.envVars!.map((o) => {
if (o.id !== e.id) {
return o;
}
if (value) {
return {
...o,
isEditting: false,
alias: value.toUpperCase(),
}
}
console.log(o);
if ("alias" in o) {
const { alias: tmp, ...rest } = o;
console.log(rest);
return {
...rest,
isEditting: false,
};
}
return {
...o,
isEditting: false,
};
}),
});
};
const saveAliasOnEnter = useCallback((e: BoundEnvVar) => {
return (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.preventDefault();
saveAlias(e, event.currentTarget.value, store);
}
}
}, [id, data, store]);
const saveAliasOnBlur = useCallback((e: BoundEnvVar) => {
return (event: FocusEvent<HTMLInputElement>) => {
saveAlias(e, event.currentTarget.value, store);
}
}, [id, data, store]);
return (
<>
<Form {...form}>
<form>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="name" className="border border-black" {...field} ref={focus(field, "name")} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value} {...typeProps}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Runtime" />
</SelectTrigger>
</FormControl>
<SelectContent>
{ServiceTypes.map((t) => (
<SelectItem key={t} value={t}>{t}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
Ports
<ul>
{data && data.ports && data.ports.map((p) => (<li key={p.id}>{p.name} - {p.value}</li>))}
</ul>
<Form {...portForm}>
<form className="flex flex-row space-x-1" onSubmit={portForm.handleSubmit(onSubmit)}>
<FormField
control={portForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="name" className="border border-black" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={portForm.control}
name="value"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="value" className="border border-black" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Add Port</Button>
</form>
</Form>
Env Vars
<ul>
{data && data.envVars && data.envVars.map((v) => {
if ("name" in v) {
const value = "alias" in v ? v.alias : v.name;
if (v.isEditting) {
return (<li key={v.id}><Input type="text" className="border border-black" defaultValue={value} onKeyUp={saveAliasOnEnter(v)} onBlur={saveAliasOnBlur(v)} autoFocus={true} /></li>);
}
return (
<li key={v.id} onClick={editAlias(v)}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button size={"icon"} variant={"ghost"}><EditIcon /></Button>
{value}
</TooltipTrigger>
<TooltipContent>
{v.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
);
}
})}
</ul>
</>);
}