blob: 1eb040948419268af7739cea1878ed80d6b73fab [file] [log] [blame]
import { v4 as uuidv4 } from "uuid";
import {
useStateStore,
AppNode,
GatewayHttpsNode,
ServiceNode,
nodeLabel,
useEnv,
nodeIsConnectable,
} from "@/lib/state";
import { Handle, Position, useNodes } from "@xyflow/react";
import { NodeRect } from "./node-rect";
import { useCallback, useEffect, useMemo } 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 { Label } from "./ui/label";
import { Button } from "./ui/button";
import { XIcon } from "lucide-react";
import { Switch } from "./ui/switch";
const schema = z.object({
network: z.string().min(1, "reqired"),
subdomain: z.string().min(1, "required"),
});
const connectedToSchema = z.object({
id: z.string(),
portId: z.string(),
});
const authEnabledSchema = z.object({
enabled: z.boolean(),
});
const authGroupSchema = z.object({
group: z.string(),
});
const authNoAuthPatternSchema = z.object({
noAuthPathPattern: z.string(),
});
export function NodeGatewayHttps(node: GatewayHttpsNode) {
const { id, selected } = node;
const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
const isConnectable = useMemo(() => nodeIsConnectable(node, "https"), [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="https"
position={Position.Bottom}
isConnectable={isConnectable}
isConnectableStart={isConnectable}
isConnectableEnd={isConnectable}
/>
</NodeRect>
);
}
export function NodeGatewayHttpsDetails({ id, data, disabled }: GatewayHttpsNode & { disabled?: boolean }) {
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) => {
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-https">(id, { network: value.network });
} else if (name === "subdomain") {
store.updateNodeData<"gateway-https">(id, { subdomain: value.subdomain });
}
},
);
return () => sub.unsubscribe();
}, [id, data, form, store]);
const network = useMemo(() => {
if (data.network === undefined) {
return null;
}
return env.networks.find((n) => n.domain === data.network)!;
}, [data, env]);
const connectedToForm = useForm<z.infer<typeof connectedToSchema>>({
resolver: zodResolver(connectedToSchema),
mode: "onChange",
defaultValues: {
id: data.https?.serviceId,
portId: data.https?.portId,
},
});
useEffect(() => {
connectedToForm.reset({
id: data.https?.serviceId,
portId: data.https?.portId,
});
}, [connectedToForm, data]);
const nodes = useNodes<AppNode>();
const selected = useMemo(() => {
if (data !== undefined && data.https !== undefined) {
const https = data.https;
return nodes.find((n) => n.id === https.serviceId)! as ServiceNode;
}
return null;
}, [data, nodes]);
const selectable = useMemo(() => {
return nodes.filter((n) => {
if (n.id === id) {
return false;
}
if (selected !== null && selected.id === id) {
return true;
}
if (n.type !== "app") {
return false;
}
return n.data && n.data.ports && n.data.ports.length > 0;
});
}, [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 "id": {
if (!value.id) {
break;
}
const current = store.edges.filter((e) => e.target === id);
const cid = current[0] ? current[0].id : undefined;
store.replaceEdge(
{
source: value.id,
sourceHandle: "ports",
target: id,
targetHandle: "https",
},
cid,
);
break;
}
case "portId":
store.updateNodeData<"gateway-https">(id, {
https: {
serviceId: value.id,
portId: value.portId,
},
});
break;
}
},
);
return () => sub.unsubscribe();
}, [id, connectedToForm, store, selectable]);
const authEnabledForm = useForm<z.infer<typeof authEnabledSchema>>({
resolver: zodResolver(authEnabledSchema),
mode: "onChange",
defaultValues: {
enabled: data.auth ? data.auth.enabled : false,
},
});
const authGroupForm = useForm<z.infer<typeof authGroupSchema>>({
resolver: zodResolver(authGroupSchema),
mode: "onSubmit",
defaultValues: {
group: "",
},
});
const authNoAuthPatternFrom = useForm<z.infer<typeof authNoAuthPatternSchema>>({
resolver: zodResolver(authNoAuthPatternSchema),
mode: "onChange",
defaultValues: {
noAuthPathPattern: "",
},
});
useEffect(() => {
const sub = authEnabledForm.watch((value, { name }) => {
if (name === "enabled") {
store.updateNodeData<"gateway-https">(id, {
auth: {
...data.auth,
enabled: value.enabled,
},
});
}
});
return () => sub.unsubscribe();
}, [id, data, authEnabledForm, store]);
const removeGroup = useCallback(
(group: string) => {
const groups = data?.auth?.groups || [];
store.updateNodeData<"gateway-https">(id, {
auth: {
...data.auth,
groups: groups.filter((g) => g !== group),
},
});
return true;
},
[id, data, store],
);
const onGroupSubmit = useCallback(
(values: z.infer<typeof authGroupSchema>) => {
const groups = data.auth?.groups || [];
groups.push(values.group);
store.updateNodeData<"gateway-https">(id, {
auth: {
...data.auth,
groups,
},
});
authGroupForm.reset();
},
[id, data, store, authGroupForm],
);
const removeNoAuthPathPattern = useCallback(
(path: string) => {
const noAuthPathPatterns = data?.auth?.noAuthPathPatterns || [];
store.updateNodeData<"gateway-https">(id, {
auth: {
...data.auth,
noAuthPathPatterns: noAuthPathPatterns.filter((p) => p !== path),
},
});
return true;
},
[id, data, store],
);
const onNoAuthPathPatternSubmit = useCallback(
(values: z.infer<typeof authNoAuthPatternSchema>) => {
const noAuthPathPatterns = data.auth?.noAuthPathPatterns || [];
noAuthPathPatterns.push(values.noAuthPathPattern);
store.updateNodeData<"gateway-https">(id, {
auth: {
...data.auth,
noAuthPathPatterns,
},
});
authNoAuthPatternFrom.reset();
},
[id, data, store, authNoAuthPatternFrom],
);
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>
<Form {...connectedToForm}>
<form className="space-y-2">
<FormField
control={connectedToForm.control}
name="id"
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 key={n.id} 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>
)}
/>
</form>
</Form>
{network?.hasAuth && (
<>
Auth
<Form {...authEnabledForm}>
<form className="space-y-2">
<FormField
control={authEnabledForm.control}
name="enabled"
render={({ field }) => (
<FormItem>
<div className="flex flex-row gap-1 items-center">
<Switch
id="authEnabled"
onCheckedChange={field.onChange}
checked={field.value}
disabled={disabled}
/>
<Label htmlFor="authEnabled">Enabled</Label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{data && data.auth && data.auth.enabled ? (
<>
Authorized Groups
<ul>
{(data.auth.groups || []).map((p) => (
<li key={p} className="flex flex-row gap-1 items-center">
<Button
size={"icon"}
variant={"ghost"}
onClick={() => removeGroup(p)}
disabled={disabled}
>
<XIcon />
</Button>
<div>{p}</div>
</li>
))}
</ul>
<Form {...authGroupForm}>
<form
className="flex flex-row space-x-1"
onSubmit={authGroupForm.handleSubmit(onGroupSubmit)}
>
<FormField
control={authGroupForm.control}
name="group"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="group" {...field} disabled={disabled} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={disabled}>
Add Group
</Button>
</form>
</Form>
Auth optional path patterns
<ul>
{(data.auth.noAuthPathPatterns || []).map((p) => (
<li key={p} className="flex flex-row gap-1 items-center">
<Button
size={"icon"}
variant={"ghost"}
onClick={() => removeNoAuthPathPattern(p)}
disabled={disabled}
>
<XIcon />
</Button>
<div>{p}</div>
</li>
))}
</ul>
<Form {...authNoAuthPatternFrom}>
<form
className="flex flex-row space-x-1"
onSubmit={authNoAuthPatternFrom.handleSubmit(onNoAuthPathPatternSubmit)}
>
<FormField
control={authNoAuthPatternFrom.control}
name="noAuthPathPattern"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="group" {...field} disabled={disabled} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={disabled}>
Add
</Button>
</form>
</Form>
</>
) : (
<></>
)}
</>
)}
</>
);
}