feat(ui): new SettingsSSLProvider using antd

This commit is contained in:
Fu Diwei
2024-12-20 20:42:46 +08:00
parent 9e1e0dee1d
commit a917d6c2c5
16 changed files with 392 additions and 488 deletions

View File

@@ -1,445 +0,0 @@
import { useContext, useEffect, useState, createContext } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { produce } from "immer";
import { cn } from "@/components/ui/utils";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useToast } from "@/components/ui/use-toast";
import { SETTINGS_NAMES, SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings";
import { get, save } from "@/repository/settings";
import { getErrMsg } from "@/utils/error";
type SSLProviderContext = {
setting: SettingsModel<SSLProviderSetting>;
onSubmit: (data: SettingsModel<SSLProviderSetting>) => void;
setConfig: (config: SettingsModel<SSLProviderSetting>) => void;
};
const Context = createContext({} as SSLProviderContext);
export const useSSLProviderContext = () => {
return useContext(Context);
};
const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => {
if (!content.config) {
return "";
}
if (!content.config[kind]) {
return "";
}
return content.config[kind][key] ?? "";
};
const SSLProvider = () => {
const { t } = useTranslation();
const [config, setConfig] = useState<SettingsModel<SSLProviderSetting>>({
content: {
provider: "letsencrypt",
config: {},
},
} as SettingsModel<SSLProviderSetting>);
const { toast } = useToast();
useEffect(() => {
const fetchData = async () => {
const setting = await get<SSLProviderSetting>(SETTINGS_NAMES.SSL_PROVIDER);
if (setting) {
setConfig(setting);
}
};
fetchData();
}, []);
const setProvider = (val: SSLProviderType) => {
const newData = produce(config, (draft) => {
if (draft.content) {
draft.content.provider = val;
} else {
draft.content = {
provider: val,
config: {},
};
}
});
setConfig(newData);
};
const getOptionCls = (val: string) => {
if (config.content?.provider === val) {
return "border-primary dark:border-primary";
}
return "";
};
const onSubmit = async (data: SettingsModel<SSLProviderSetting>) => {
try {
console.log(data);
const resp = await save({ ...data });
setConfig(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("common.text.operation_succeeded"),
});
} catch (e) {
const message = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: message,
variant: "destructive",
});
}
};
return (
<>
<Context.Provider value={{ onSubmit, setConfig, setting: config }}>
<div className="md:max-w-[40rem]">
<Label className="dark:text-stone-200">{t("common.text.ca")}</Label>
<RadioGroup
className="flex mt-3 dark:text-stone-200"
onValueChange={(val) => {
setProvider(val as SSLProviderType);
}}
value={config.content?.provider}
>
<div className="flex items-center space-x-2 ">
<RadioGroupItem value="letsencrypt" id="letsencrypt" />
<Label htmlFor="letsencrypt">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("letsencrypt"))}>
<img src={"/imgs/providers/letsencrypt.svg"} className="h-6" />
<div>{"Let's Encrypt"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2 ">
<RadioGroupItem value="zerossl" id="zerossl" />
<Label htmlFor="zerossl">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("zerossl"))}>
<img src={"/imgs/providers/zerossl.svg"} className="h-6" />
<div>{"ZeroSSL"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gts" id="gts" />
<Label htmlFor="gts">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("gts"))}>
<img src={"/imgs/providers/google.svg"} className="h-6" />
<div>{"Google Trust Services"}</div>
</div>
</Label>
</div>
</RadioGroup>
<SSLProviderForm kind={config.content?.provider ?? ""} />
</div>
</Context.Provider>
</>
);
};
const SSLProviderForm = ({ kind }: { kind: string }) => {
const getForm = () => {
switch (kind) {
case "zerossl":
return <SSLProviderZeroSSLForm />;
case "gts":
return <SSLProviderGoogleTrustServicesForm />;
default:
return <SSLProviderLetsEncryptForm />;
}
};
return (
<>
<div className="mt-5">{getForm()}</div>
</>
);
};
const SSLProviderLetsEncryptForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("letsencrypt"),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "letsencrypt",
},
});
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: data.kind,
config: {
letsencrypt: {},
},
};
}
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.button.save")}</Button>
</div>
</form>
</Form>
);
};
const SSLProviderZeroSSLForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("zerossl"),
eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "zerossl",
eabKid: "",
eabHmacKey: "",
},
});
useEffect(() => {
if (setting.content) {
const content = setting.content;
form.reset({
eabKid: getConfigStr(content, "zerossl", "eabKid"),
eabHmacKey: getConfigStr(content, "zerossl", "eabHmacKey"),
});
}
}, [setting]);
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: "zerossl",
config: {
zerossl: {},
},
};
}
draft.content.config.zerossl = {
eabKid: data.eabKid,
eabHmacKey: data.eabHmacKey,
};
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.button.save")}</Button>
</div>
</form>
</Form>
);
};
const SSLProviderGoogleTrustServicesForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("gts"),
eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "gts",
eabKid: "",
eabHmacKey: "",
},
});
useEffect(() => {
if (setting.content) {
const content = setting.content;
form.reset({
eabKid: getConfigStr(content, "gts", "eabKid"),
eabHmacKey: getConfigStr(content, "gts", "eabHmacKey"),
});
}
}, [setting]);
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: "gts",
config: {
zerossl: {},
},
};
}
draft.content.config.gts = {
eabKid: data.eabKid,
eabHmacKey: data.eabHmacKey,
};
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.button.save")}</Button>
</div>
</form>
</Form>
);
};
export default SSLProvider;

View File

@@ -60,7 +60,7 @@ const Settings = () => {
label: (
<Space>
<ShieldCheckIcon size={14} />
<label>{t("settings.ca.tab")}</label>
<label>{t("settings.sslprovider.tab")}</label>
</Space>
),
},

View File

@@ -0,0 +1,301 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Form, Input, message, notification, Skeleton } from "antd";
import { CheckCard } from "@ant-design/pro-components";
import { createSchemaFieldRule } from "antd-zod";
import { produce } from "immer";
import { z } from "zod";
import { SETTINGS_NAMES, SSLPROVIDERS, type SettingsModel, type SSLProviderSettingsContent, type SSLProviders } from "@/domain/settings";
import { get as getSettings, save as saveSettings } from "@/repository/settings";
import { getErrMsg } from "@/utils/error";
import { useDeepCompareEffect } from "ahooks";
const SSLProviderContext = createContext(
{} as {
pending: boolean;
settings: SettingsModel<SSLProviderSettingsContent>;
updateSettings: (settings: MaybeModelRecordWithId<SettingsModel<SSLProviderSettingsContent>>) => Promise<void>;
}
);
const SSLProviderEditFormLetsEncryptConfig = () => {
const { t } = useTranslation();
const { pending, settings, updateSettings } = useContext(SSLProviderContext);
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.LETS_ENCRYPT]);
const [initialChanged, setInitialChanged] = useState(false);
useDeepCompareEffect(() => {
setInitialValues(settings?.content?.config?.[SSLPROVIDERS.LETS_ENCRYPT]);
setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.LETS_ENCRYPT);
}, [settings]);
const handleFormChange = () => {
setInitialChanged(true);
};
const handleFormFinish = async (fields: NonNullable<unknown>) => {
const newSettings = produce(settings, (draft) => {
draft.content ??= {} as SSLProviderSettingsContent;
draft.content.provider = SSLPROVIDERS.LETS_ENCRYPT;
draft.content.config ??= {} as SSLProviderSettingsContent["config"];
draft.content.config[SSLPROVIDERS.LETS_ENCRYPT] = fields;
});
await updateSettings(newSettings);
setInitialChanged(false);
};
return (
<Form form={form} disabled={pending} layout="vertical" initialValues={initialValues} onFinish={handleFormFinish} onValuesChange={handleFormChange}>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={pending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
);
};
const SSLProviderEditFormZeroSSLConfig = () => {
const { t } = useTranslation();
const { pending, settings, updateSettings } = useContext(SSLProviderContext);
const formSchema = z.object({
eabKid: z
.string({ message: t("settings.sslprovider.form.zerossl_eab_kid.placeholder") })
.min(1, t("settings.sslprovider.form.zerossl_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
eabHmacKey: z
.string({ message: t("settings.sslprovider.form.zerossl_eab_hmac_key.placeholder") })
.min(1, t("settings.sslprovider.form.zerossl_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
});
const formRule = createSchemaFieldRule(formSchema);
const [form] = Form.useForm<z.infer<typeof formSchema>>();
const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.ZERO_SSL]);
const [initialChanged, setInitialChanged] = useState(false);
useDeepCompareEffect(() => {
setInitialValues(settings?.content?.config?.[SSLPROVIDERS.ZERO_SSL]);
setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.ZERO_SSL);
}, [settings]);
const handleFormChange = () => {
setInitialChanged(true);
};
const handleFormFinish = async (fields: z.infer<typeof formSchema>) => {
const newSettings = produce(settings, (draft) => {
draft.content ??= {} as SSLProviderSettingsContent;
draft.content.provider = SSLPROVIDERS.ZERO_SSL;
draft.content.config ??= {} as SSLProviderSettingsContent["config"];
draft.content.config[SSLPROVIDERS.ZERO_SSL] = fields;
});
await updateSettings(newSettings);
setInitialChanged(false);
};
return (
<Form form={form} disabled={pending} layout="vertical" initialValues={initialValues} onFinish={handleFormFinish} onValuesChange={handleFormChange}>
<Form.Item
name="eabKid"
label={t("settings.sslprovider.form.zerossl_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.zerossl_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("settings.sslprovider.form.zerossl_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("settings.sslprovider.form.zerossl_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.zerossl_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.sslprovider.form.zerossl_eab_hmac_key.placeholder")} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={pending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
);
};
const SSLProviderEditFormGoogleTrustServicesConfig = () => {
const { t } = useTranslation();
const { pending, settings, updateSettings } = useContext(SSLProviderContext);
const formSchema = z.object({
eabKid: z
.string({ message: t("settings.sslprovider.form.gts_eab_kid.placeholder") })
.min(1, t("settings.sslprovider.form.gts_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
eabHmacKey: z
.string({ message: t("settings.sslprovider.form.gts_eab_hmac_key.placeholder") })
.min(1, t("settings.sslprovider.form.gts_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
});
const formRule = createSchemaFieldRule(formSchema);
const [form] = Form.useForm<z.infer<typeof formSchema>>();
const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.GOOGLE_TRUST_SERVICES]);
const [initialChanged, setInitialChanged] = useState(false);
useDeepCompareEffect(() => {
setInitialValues(settings?.content?.config?.[SSLPROVIDERS.GOOGLE_TRUST_SERVICES]);
setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.GOOGLE_TRUST_SERVICES);
}, [settings]);
const handleFormChange = () => {
setInitialChanged(true);
};
const handleFormFinish = async (fields: z.infer<typeof formSchema>) => {
const newSettings = produce(settings, (draft) => {
draft.content ??= {} as SSLProviderSettingsContent;
draft.content.provider = SSLPROVIDERS.GOOGLE_TRUST_SERVICES;
draft.content.config ??= {} as SSLProviderSettingsContent["config"];
draft.content.config[SSLPROVIDERS.GOOGLE_TRUST_SERVICES] = fields;
});
await updateSettings(newSettings);
setInitialChanged(false);
};
return (
<Form form={form} disabled={pending} layout="vertical" initialValues={initialValues} onFinish={handleFormFinish} onValuesChange={handleFormChange}>
<Form.Item
name="eabKid"
label={t("settings.sslprovider.form.gts_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.gts_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("settings.sslprovider.form.gts_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("settings.sslprovider.form.gts_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.gts_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.sslprovider.form.gts_eab_hmac_key.placeholder")} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={pending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
);
};
const SettingsSSLProvider = () => {
const { t } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const [form] = Form.useForm();
const [formPending, setFormPending] = useState(false);
const [settings, setSettings] = useState<SettingsModel<SSLProviderSettingsContent>>();
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const settings = await getSettings<SSLProviderSettingsContent>(SETTINGS_NAMES.SSL_PROVIDER);
setSettings(settings);
setFormProviderType(settings.content?.provider);
setLoading(false);
};
fetchData();
}, []);
const [providerType, setFormProviderType] = useState<SSLProviders>();
const providerFormComponent = useMemo(() => {
switch (providerType) {
case SSLPROVIDERS.LETS_ENCRYPT:
return <SSLProviderEditFormLetsEncryptConfig />;
case SSLPROVIDERS.ZERO_SSL:
return <SSLProviderEditFormZeroSSLConfig />;
case SSLPROVIDERS.GOOGLE_TRUST_SERVICES:
return <SSLProviderEditFormGoogleTrustServicesConfig />;
}
}, [providerType]);
const updateContextSettings = async (settings: MaybeModelRecordWithId<SettingsModel<SSLProviderSettingsContent>>) => {
setFormPending(true);
try {
const resp = await saveSettings(settings);
setSettings(resp);
setFormProviderType(resp.content?.provider);
messageApi.success(t("common.text.operation_succeeded"));
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
} finally {
setFormPending(false);
}
};
return (
<SSLProviderContext.Provider
value={{
pending: formPending,
settings: settings!,
updateSettings: updateContextSettings,
}}
>
{MessageContextHolder}
{NotificationContextHolder}
{loading ? (
<Skeleton active />
) : (
<>
<Form form={form} disabled={formPending} layout="vertical" initialValues={{ provider: providerType }}>
<Form.Item className="mb-2" name="provider" label={t("settings.sslprovider.form.provider.label")} initialValue={SSLPROVIDERS.LETS_ENCRYPT}>
<CheckCard.Group className="w-full" onChange={(value) => setFormProviderType(value as SSLProviders)}>
<CheckCard
avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />}
size="small"
title="Let's Encrypt"
value={SSLPROVIDERS.LETS_ENCRYPT}
/>
<CheckCard avatar={<img src={"/imgs/acme/zerossl.svg"} className="size-8" />} size="small" title="ZeroSSL" value={SSLPROVIDERS.ZERO_SSL} />
<CheckCard
avatar={<img src={"/imgs/acme/google.svg"} className="size-8" />}
size="small"
title="Google Trust Services"
value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES}
/>
</CheckCard.Group>
</Form.Item>
</Form>
<div className="md:max-w-[40rem]">{providerFormComponent}</div>
</>
)}
</SSLProviderContext.Provider>
);
};
export default SettingsSSLProvider;