1import { type FC, useEffect } from "react";
2import {
3 Button,
4 Dialog,
5 DialogContent,
6 DialogHeader,
7 DialogTitle,
8 Form,
9 ScrollArea,
10} from "@marzneshin/common/components";
11import { useForm } from "react-hook-form";
12import { zodResolver } from "@hookform/resolvers/zod";
13import { useTranslation } from "react-i18next";
14import {
15 type HostWithProfileSchemaType,
16 type HostWithProfileType,
17 useHostsCreationMutation,
18 useHostsUpdateMutation,
19} from "@marzneshin/modules/hosts";
20import { type MutationDialogProps, useDialog } from "@marzneshin/common/hooks";
21import { ProtocolType } from "@marzneshin/modules/inbounds";
22import { useProfileStrategy } from "./profiles";
23import {
24 transformToDictionary,
25 transformToFields,
26} from "@marzneshin/libs/dynamic-field";
27
28interface HostMutationDialogProps
29 extends MutationDialogProps<HostWithProfileType> {
30 inboundId?: number;
31 protocol?: ProtocolType;
32}
33
34const transformFormValue = (values: any) => {
35 if (!(values.alpn && values.fingerprint)) return values;
36 const port = values.port === "" ? null : values.port;
37 const alpn = values.alpn === "none" ? "" : values.alpn;
38 const fingerprint = values.fingerprint === "none" ? "" : values.fingerprint;
39 const http_headers = values.http_headers;
40 return { ...values, alpn, fingerprint, port, http_headers };
41};
42
43const transformFormValueHttpHeaders = (values: any) => {
44 const http_headers = values.http_headers
45 ? transformToDictionary(values.http_headers)
46 : undefined;
47 return { ...values, http_headers };
48};
49
50const formMutationToAPIAdapter = (values: HostWithProfileType) => {
51 if (values.http_headers)
52 return {
53 ...values,
54 http_headers: transformToFields(values.http_headers),
55 };
56 return values;
57};
58
59export const HostsMutationDialog: FC<HostMutationDialogProps> = ({
60 entity,
61 inboundId,
62 onClose,
63 protocol,
64}) => {
65 const [open, onOpenChange] = useDialog(true);
66 const [Schema, ProfileFields, defaultValue] = useProfileStrategy(protocol);
67 const form = useForm<HostWithProfileSchemaType>({
68 defaultValues: entity ? formMutationToAPIAdapter(entity) : defaultValue,
69 resolver: zodResolver(Schema),
70 });
71 const updateMutation = useHostsUpdateMutation();
72 const createMutation = useHostsCreationMutation();
73 const { t } = useTranslation();
74
75 // TODO: Refactor value transformation for api using of
76 // function composition to pipes and port-adapter patterns.
77 // - [ ] ALPN and fingerprint
78 // - [ ] HTTP headers (duplex adaptation)
79 const submit = (values: HostWithProfileSchemaType) => {
80 const host = transformFormValueHttpHeaders(transformFormValue(values));
81 if (entity && entity.id !== undefined) {
82 updateMutation.mutate({ hostId: entity.id, host });
83 onOpenChange(false);
84 } else if (inboundId !== undefined) {
85 createMutation.mutate({ inboundId, host });
86 onOpenChange(false);
87 }
88 };
89
90 useEffect(() => {
91 if (!open) onClose();
92 }, [open, onClose]);
93
94 useEffect(() => {
95 if (entity) form.reset(formMutationToAPIAdapter(entity));
96 else form.reset(defaultValue);
97 }, [entity, form, open, defaultValue]);
98
99 return (
100 <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
101 <DialogContent className="min-w-full h-full md:h-auto md:min-w-[32rem]">
102 <ScrollArea className="flex flex-col h-full p-0">
103 <DialogHeader className="mb-3">
104 <DialogTitle className="text-primary">
105 {entity
106 ? t("page.hosts.dialogs.edition.title")
107 : t("page.hosts.dialogs.creation.title")}
108 </DialogTitle>
109 </DialogHeader>
110 <Form {...form}>
111 <form
112 onSubmit={form.handleSubmit(submit)}
113 className="h-full flex flex-col justify-between"
114 >
115 <ProfileFields />
116 <Button
117 className="mt-3 w-full font-semibold"
118 type="submit"
119 disabled={form.formState.isSubmitting}
120 >
121 {t("submit")}
122 </Button>
123 </form>
124 </Form>
125 </ScrollArea>
126 </DialogContent>
127 </Dialog>
128 );
129};