gitscope
← marzneshin / v0.7.2 / dashboard/src/modules/hosts/dialogs/mutation/index.tsx
https://dev.kronoz.win/r/marzneshin.git
dashboard/src/modules/hosts/dialogs/mutation/index.tsx · TypeScript
Blame Raw
  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};