blob: 919bf839b1f7f00a3dfc0b2edaca819d75c166f7 [file] [log] [blame]
import { v4 as uuidv4 } from "uuid";
import { useStateStore, AppNode, nodeLabel, useEnv, GatewayTCPNode, nodeIsConnectable } from "@/lib/state";
import { Edge, Handle, Position, useNodes } from "@xyflow/react";
import { NodeRect } from "./node-rect";
import { useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, EventType, DeepPartial } from "react-hook-form";
import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Button } from "./ui/button";
import { NodeDetailsProps } from "@/lib/types";
const schema = z.object({
network: z.string().min(1, "reqired"),
subdomain: z.string().min(1, "required"),
});
const connectedToSchema = z.object({
serviceId: z.string(),
portId: z.string(),
});
export function NodeGatewayTCP(node: GatewayTCPNode) {
const { id, selected } = node;
const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
return (
<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
{nodeLabel(node)}
<Handle
type={"source"}
id="subdomain"
position={Position.Top}
isConnectable={isConnectableNetwork}
isConnectableStart={isConnectableNetwork}
isConnectableEnd={isConnectableNetwork}
/>
<Handle
type={"target"}
id="tcp"
position={Position.Bottom}
isConnectable={isConnectable}
isConnectableStart={isConnectable}
isConnectableEnd={isConnectable}
/>
</NodeRect>
);
}
export function NodeGatewayTCPDetails({ node, disabled }: NodeDetailsProps<GatewayTCPNode>) {
const { id, data } = node;
const store = useStateStore();
const env = useEnv();
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
mode: "onChange",
defaultValues: {
network: data.network,
subdomain: data.subdomain,
},
});
useEffect(() => {
const sub = form.watch(
(
value: DeepPartial<z.infer<typeof schema>>,
{ name }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
) => {
if (name === "network") {
let edges = store.edges;
if (data.network !== undefined) {
edges = edges.filter((e) => {
console.log(e);
if (
e.source === id &&
e.sourceHandle === "subdomain" &&
e.target === data.network &&
e.targetHandle === "subdomain"
) {
return false;
} else {
return true;
}
});
}
if (value.network !== undefined) {
edges = edges.concat({
id: uuidv4(),
source: id,
sourceHandle: "subdomain",
target: value.network,
targetHandle: "subdomain",
});
}
store.setEdges(edges);
store.updateNodeData<"gateway-tcp">(id, { network: value.network });
} else if (name === "subdomain") {
store.updateNodeData<"gateway-tcp">(id, { subdomain: value.subdomain });
}
},
);
return () => sub.unsubscribe();
}, [id, data, form, store]);
const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
resolver: zodResolver(connectedToSchema),
mode: "onSubmit",
defaultValues: {
serviceId: data.selected?.serviceId,
portId: data.selected?.portId,
},
});
useEffect(() => {
connectedToForm.reset({
serviceId: data.selected?.serviceId,
portId: data.selected?.portId,
});
console.log(connectedToForm.getValues());
}, [id, connectedToForm, data]);
const nodes = useNodes<AppNode>();
const [selected, setSelected] = useState<AppNode | undefined>(undefined);
useEffect(() => {
if (data.selected?.serviceId == null) {
setSelected(undefined);
} else {
const serviceId = data.selected.serviceId;
setSelected(nodes.find((n) => n.id === serviceId));
}
}, [id, data, setSelected, nodes]);
const selectable = useMemo(() => {
console.log(selected);
return nodes.filter((n) => {
if (n.id === id) {
return false;
}
if (selected != null && selected.id === id) {
return true;
}
if ("ports" in n.data && (n.data.ports || []).length > 0) {
return true;
}
return false;
});
}, [id, nodes, selected]);
useEffect(() => {
const sub = connectedToForm.watch(
(
value: DeepPartial<z.infer<typeof connectedToSchema>>,
{
name,
type,
}: { name?: keyof z.infer<typeof connectedToSchema> | undefined; type?: EventType | undefined },
) => {
if (type !== "change") {
return;
}
switch (name) {
case "serviceId":
if (!value.serviceId) {
break;
}
store.updateNodeData<"gateway-tcp">(id, {
selected: {
serviceId: value.serviceId,
},
});
break;
case "portId":
if (!value.portId) {
break;
}
store.updateNodeData<"gateway-tcp">(id, {
selected: {
serviceId: value.serviceId,
portId: value.portId,
},
});
break;
}
},
);
return () => sub.unsubscribe();
}, [id, connectedToForm, store]);
const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
const [portLabels, setPortLabels] = useState(new Map<string, string>());
useEffect(() => {
setNodeLabels(
new Map(
(data.exposed || []).map((e) => [e.serviceId, nodeLabel(nodes.find((n) => n.id === e.serviceId)!)]),
),
);
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,
]),
),
);
}, [nodes, data, setNodeLabels, setPortLabels]);
const onSubmit = useCallback(
(values: z.infer<typeof connectedToSchema>) => {
const edges = store.edges.filter((e) => e.target !== id);
const exp = (data.exposed || []).concat({
serviceId: values.serviceId,
portId: values.portId,
});
store.updateNodeData<"gateway-tcp">(id, {
exposed: exp,
selected: undefined,
});
store.setEdges(
edges.concat(
exp.map((e): Edge => {
const sn = nodes.find((n) => n.id === e.serviceId);
if (sn == null) {
throw new Error(`Service ${e.serviceId} not found`);
}
if (sn.type === "app") {
return {
id: uuidv4(),
source: e.serviceId,
sourceHandle: "ports",
target: id,
targetHandle: "tcp",
};
} else {
return {
id: uuidv4(),
source: e.serviceId,
sourceHandle: "env_var",
target: id,
targetHandle: "tcp",
};
}
}),
),
);
},
[id, data, store, nodes],
);
return (
<>
<Form {...form}>
<form className="space-y-2">
<FormField
control={form.control}
name="network"
render={({ field }) => (
<FormItem>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
disabled={data.readonly || disabled}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Network" />
</SelectTrigger>
</FormControl>
<SelectContent>
{env.networks.map((n) => (
<SelectItem
key={n.name}
value={n.domain}
>{`${n.name} - ${n.domain}`}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="subdomain" {...field} disabled={data.readonly || disabled} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
Exposed Services
<ul>
{(data.exposed || []).map((e, i) => (
<li key={i}>
{nodeLabels.get(e.serviceId)} - {portLabels.get(`${e.serviceId} - ${e.portId}`)}
</li>
))}
</ul>
<Form {...connectedToForm}>
<form className="space-y-2" onSubmit={connectedToForm.handleSubmit(onSubmit)}>
<FormField
control={connectedToForm.control}
name="serviceId"
render={({ field }) => (
<FormItem>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
disabled={data.readonly || disabled}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Service" />
</SelectTrigger>
</FormControl>
<SelectContent>
{selectable.map((n) => (
<SelectItem value={n.id}>{nodeLabel(n)}</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={connectedToForm.control}
name="portId"
render={({ field }) => (
<FormItem>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
disabled={data.readonly || disabled}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Port" />
</SelectTrigger>
</FormControl>
<SelectContent>
{selected &&
(selected.data.ports || []).map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name} - {p.value}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={disabled}>
Expose
</Button>
</form>
</Form>
</>
);
}