This commit is contained in:
yoan 2024-10-10 11:13:45 +08:00
parent 30ef5841d6
commit 9f1b00f04c
6 changed files with 301 additions and 420 deletions

View File

@ -20,13 +20,14 @@ import { Edit, Plus, Trash2 } from "lucide-react";
type StringListProps = { type StringListProps = {
className?: string; className?: string;
value: string; value: string;
valueType?: "domain" | "ip"; valueType?: ValueType;
onValueChange: (value: string) => void; onValueChange: (value: string) => void;
}; };
const titles: Record<string, string> = { const titles: Record<string, string> = {
domain: "domain", domain: "domain",
ip: "IP", ip: "IP",
dns: "dns",
}; };
const StringList = ({ const StringList = ({
@ -100,7 +101,9 @@ const StringList = ({
when={list.length > 0} when={list.length > 0}
fallback={ fallback={
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center"> <div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
<div className="text-muted-foreground"></div> <div className="text-muted-foreground">
{t("not.added.yet." + valueType)}
</div>
<StringEdit <StringEdit
value={""} value={""}
@ -150,7 +153,7 @@ const StringList = ({
export default StringList; export default StringList;
type ValueType = "domain" | "ip"; type ValueType = "domain" | "dns" | "host";
type StringEditProps = { type StringEditProps = {
value: string; value: string;
@ -186,7 +189,8 @@ const StringEdit = ({
const schedules: Record<ValueType, z.ZodString> = { const schedules: Record<ValueType, z.ZodString> = {
domain: domainSchema, domain: domainSchema,
ip: ipSchema, dns: ipSchema,
host: ipSchema,
}; };
const onSaveClick = useCallback(() => { const onSaveClick = useCallback(() => {

View File

@ -7,7 +7,7 @@ export type Domain = {
crontab: string; crontab: string;
access: string; access: string;
targetAccess?: string; targetAccess?: string;
targetType: string; targetType?: string;
expiredAt?: string; expiredAt?: string;
phase?: Pahse; phase?: Pahse;
phaseSuccess?: boolean; phaseSuccess?: boolean;
@ -31,10 +31,19 @@ export type Domain = {
deployConfig?: DeployConfig[]; deployConfig?: DeployConfig[];
}; };
type KVType = {
key: string;
value: string;
};
export type DeployConfig = { export type DeployConfig = {
access: string; access: string;
type: string; type: string;
config?: Record<string, string>; config?: {
[key: string]: string;
} & {
variables?: KVType[];
};
}; };
export type ApplyConfig = { export type ApplyConfig = {

View File

@ -4,12 +4,14 @@
"username.not.empty": "Please enter username", "username.not.empty": "Please enter username",
"password": "Password", "password": "Password",
"password.not.empty": "Please enter password", "password.not.empty": "Please enter password",
"ip.not.empty.verify.message": "Please enter Ip",
"email": "Email", "email": "Email",
"logout": "Logout", "logout": "Logout",
"setting": "Settings", "setting": "Settings",
"account": "Account", "account": "Account",
"template": "Template", "template": "Template",
"save": "Save", "save": "Save",
"next": "Next",
"no.data": "No data available", "no.data": "No data available",
"status": "Status", "status": "Status",
"operation": "Operation", "operation": "Operation",
@ -28,12 +30,15 @@
"variables": "Variables", "variables": "Variables",
"dns": "Domain Name Server", "dns": "Domain Name Server",
"name": "Name", "name": "Name",
"timeout": "Time Out",
"not.added.yet.domain": "Domain not added yet.",
"not.added.yet.dns": "Nameserver not added yet.",
"create.time": "CreateTime", "create.time": "CreateTime",
"update.time": "UpdateTime", "update.time": "UpdateTime",
"created.in": "Created in", "created.in": "Created in",
"updated.in": "Updated in", "updated.in": "Updated in",
"basic.setting": "Basic Settings", "apply.setting": "Apply Settings",
"advanced.setting": "Advanced Settings", "deploy.setting": "Deploy Settings",
"operation.succeed": "Operation Successful", "operation.succeed": "Operation Successful",
"save.succeed": "Save Successful", "save.succeed": "Save Successful",
"save.failed": "Save Failed", "save.failed": "Save Failed",
@ -83,7 +88,7 @@
"pagination.prev": "Previous", "pagination.prev": "Previous",
"domain": "Domain", "domain": "Domain",
"domain.add": "Add Domain", "domain.add": "Add Domain",
"domain.edit":"Edit Domain", "domain.edit": "Edit Domain",
"domain.delete": "Delete Domain", "domain.delete": "Delete Domain",
"domain.not.empty.verify.message": "Please enter domain", "domain.not.empty.verify.message": "Please enter domain",
"domain.management.name": "Domain List", "domain.management.name": "Domain List",
@ -120,6 +125,10 @@
"domain.management.edit.variables.placeholder": "It can be used in SSH deployment, like:\nkey=val;\nkey2=val2;", "domain.management.edit.variables.placeholder": "It can be used in SSH deployment, like:\nkey=val;\nkey2=val2;",
"domain.management.edit.dns.placeholder": "Custom domain name server, separates multiple entries with semicolon, like:\n8.8.8.8;\n8.8.4.4;", "domain.management.edit.dns.placeholder": "Custom domain name server, separates multiple entries with semicolon, like:\n8.8.8.8;\n8.8.4.4;",
"domain.management.add.succeed.tips": "Domain added successfully", "domain.management.add.succeed.tips": "Domain added successfully",
"domain.management.edit.timeout.placeholder": "Timeout (seconds)",
"domain.management.edit.deploy.error": "Please save applyment configuration first",
"domain.management.enabled.failed": "Enable failed",
"domain.management.enabled.without.deployments": "Failed to enable, no deployment configuration found",
"email.add": "Add Email", "email.add": "Add Email",
"email.list": "Email List", "email.list": "Email List",
"email.valid.message": "Please enter a valid email address", "email.valid.message": "Please enter a valid email address",

View File

@ -4,12 +4,14 @@
"username.not.empty": "请输入用户名", "username.not.empty": "请输入用户名",
"password": "密码", "password": "密码",
"password.not.empty": "请输入密码", "password.not.empty": "请输入密码",
"ip.not.empty.verify.message": "请输入 IP",
"email": "邮箱", "email": "邮箱",
"logout": "退出登录", "logout": "退出登录",
"setting": "设置", "setting": "设置",
"account": "账户", "account": "账户",
"template": "模版", "template": "模版",
"save": "保存", "save": "保存",
"next": "下一步",
"no.data": "暂无数据", "no.data": "暂无数据",
"status": "状态", "status": "状态",
"operation": "操作", "operation": "操作",
@ -28,12 +30,15 @@
"variables": "变量", "variables": "变量",
"dns": "域名服务器", "dns": "域名服务器",
"name": "名称", "name": "名称",
"timeout": "超时时间",
"not.added.yet.domain": "域名未添加",
"not.added.yet.dns": "域名服务器暂未添加",
"create.time": "创建时间", "create.time": "创建时间",
"update.time": "更新时间", "update.time": "更新时间",
"created.in": "创建于", "created.in": "创建于",
"updated.in": "更新于", "updated.in": "更新于",
"basic.setting": "基础设置", "apply.setting": "申请设置",
"advanced.setting": "高级设置", "deploy.setting": "部署设置",
"operation.succeed": "操作成功", "operation.succeed": "操作成功",
"save.succeed": "保存成功", "save.succeed": "保存成功",
"save.failed": "保存失败", "save.failed": "保存失败",
@ -120,6 +125,10 @@
"domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;", "domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;",
"domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;", "domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;",
"domain.management.add.succeed.tips": "域名添加成功", "domain.management.add.succeed.tips": "域名添加成功",
"domain.management.edit.timeout.placeholder": "超时时间(单位:秒)",
"domain.management.edit.deploy.error": "请先保存申请配置",
"domain.management.enabled.failed": "启用失败",
"domain.management.enabled.without.deployments": "启用失败,请先设置部署配置",
"email.add": "添加邮箱", "email.add": "添加邮箱",
"email.list": "邮箱列表", "email.list": "邮箱列表",
"email.valid.message": "请输入正确的邮箱地址", "email.valid.message": "请输入正确的邮箱地址",

View File

@ -30,7 +30,7 @@ import { PbErrorData } from "@/domain/base";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Plus } from "lucide-react"; import { Plus, Trash2, Edit as EditIcon } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit"; import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access"; import { accessTypeMap } from "@/domain/access";
import EmailsEdit from "@/components/certimate/EmailsEdit"; import EmailsEdit from "@/components/certimate/EmailsEdit";
@ -39,6 +39,7 @@ import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings"; import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import StringList from "@/components/certimate/StringList"; import StringList from "@/components/certimate/StringList";
import { Input } from "@/components/ui/input";
const Edit = () => { const Edit = () => {
const { const {
@ -50,7 +51,7 @@ const Edit = () => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const [tab, setTab] = useState<"base" | "advance">("base"); const [tab, setTab] = useState<"apply" | "deploy">("apply");
const [targetType, setTargetType] = useState(domain ? domain.targetType : ""); const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
@ -77,13 +78,8 @@ const Edit = () => {
access: z.string().regex(/^[a-zA-Z0-9]+$/, { access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "domain.management.edit.dns.access.not.empty.message", message: "domain.management.edit.dns.access.not.empty.message",
}), }),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "domain.management.edit.target.type.not.empty.message",
}),
variables: z.string().optional(),
group: z.string().optional(),
nameservers: z.string().optional(), nameservers: z.string().optional(),
timeout: z.number().optional(),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -93,11 +89,8 @@ const Edit = () => {
domain: "", domain: "",
email: "", email: "",
access: "", access: "",
targetAccess: "",
targetType: "",
variables: "",
group: "",
nameservers: "", nameservers: "",
timeout: 60,
}, },
}); });
@ -106,13 +99,11 @@ const Edit = () => {
form.reset({ form.reset({
id: domain.id, id: domain.id,
domain: domain.domain, domain: domain.domain,
email: domain.email, email: domain.applyConfig?.email,
access: domain.access, access: domain.applyConfig?.access,
targetAccess: domain.targetAccess,
targetType: domain.targetType, nameservers: domain.applyConfig?.nameservers,
variables: domain.variables, timeout: domain.applyConfig?.timeout,
group: domain.group,
nameservers: domain.nameservers,
}); });
} }
}, [domain, form]); }, [domain, form]);
@ -134,32 +125,19 @@ const Edit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const onSubmit = async (data: z.infer<typeof formSchema>) => { const onSubmit = async (data: z.infer<typeof formSchema>) => {
const group = data.group == "emptyId" ? "" : data.group; console.log(data);
const targetAccess =
data.targetAccess === "emptyId" ? "" : data.targetAccess;
if (group == "" && targetAccess == "") {
form.setError("group", {
type: "manual",
message: "domain.management.edit.target.access.verify.msg",
});
form.setError("targetAccess", {
type: "manual",
message: "domain.management.edit.target.access.verify.msg",
});
return;
}
const req: Domain = { const req: Domain = {
id: data.id as string, id: data.id as string,
crontab: "0 0 * * *", crontab: "0 0 * * *",
domain: data.domain, domain: data.domain,
email: data.email, email: data.email,
access: data.access, access: data.access,
group: group, applyConfig: {
targetAccess: targetAccess, email: data.email ?? "",
targetType: data.targetType, access: data.access,
variables: data.variables,
nameservers: data.nameservers, nameservers: data.nameservers,
timeout: data.timeout,
},
}; };
try { try {
@ -173,7 +151,7 @@ const Edit = () => {
title: t("succeed"), title: t("succeed"),
description, description,
}); });
navigate("/domains"); if (!domain?.id) setTab("deploy");
} catch (e) { } catch (e) {
const err = e as ClientResponseError; const err = e as ClientResponseError;
@ -198,42 +176,56 @@ const Edit = () => {
{domain?.id ? t("domain.edit") : t("domain.add")} {domain?.id ? t("domain.edit") : t("domain.add")}
</div> </div>
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row"> <div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex"> <div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
<div <div
className={cn( className={cn(
"cursor-pointer text-right", "cursor-pointer text-right",
tab === "base" ? "text-primary" : "" tab === "apply" ? "text-primary" : ""
)} )}
onClick={() => { onClick={() => {
setTab("base"); setTab("apply");
}} }}
> >
{t("basic.setting")} {t("apply.setting")}
</div> </div>
<div <div
className={cn( className={cn(
"cursor-pointer text-right", "cursor-pointer text-right",
tab === "advance" ? "text-primary" : "" tab === "deploy" ? "text-primary" : ""
)} )}
onClick={() => { onClick={() => {
setTab("advance"); if (!domain?.id) {
toast({
title: t("domain.management.edit.deploy.error"),
description: t("domain.management.edit.deploy.error"),
variant: "destructive",
});
return;
}
setTab("deploy");
}} }}
> >
{t("advanced.setting")} {t("deploy.setting")}
</div> </div>
</div> </div>
<div className="flex flex-col">
<div className="w-full md:w-[35em] bg-gray-100 dark:bg-gray-900 p-5 rounded mt-3 md:mt-0"> <div
className={cn(
"w-full md:w-[35em] p-5 rounded mt-3 md:mt-0",
tab == "deploy" && "hidden"
)}
>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200" className="space-y-8 dark:text-stone-200"
> >
{/* 域名 */}
<FormField <FormField
control={form.control} control={form.control}
name="domain" name="domain"
render={({ field }) => ( render={({ field }) => (
<FormItem hidden={tab != "base"}> <FormItem>
<> <>
<StringList <StringList
value={field.value} value={field.value}
@ -248,11 +240,12 @@ const Edit = () => {
</FormItem> </FormItem>
)} )}
/> />
{/* 邮箱 */}
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem hidden={tab != "base"}> <FormItem>
<FormLabel className="flex w-full justify-between"> <FormLabel className="flex w-full justify-between">
<div> <div>
{t("email") + {t("email") +
@ -301,11 +294,12 @@ const Edit = () => {
</FormItem> </FormItem>
)} )}
/> />
{/* 授权 */}
<FormField <FormField
control={form.control} control={form.control}
name="access" name="access"
render={({ field }) => ( render={({ field }) => (
<FormItem hidden={tab != "base"}> <FormItem>
<FormLabel className="flex w-full justify-between"> <FormLabel className="flex w-full justify-between">
<div> <div>
{t("domain.management.edit.dns.access.label")} {t("domain.management.edit.dns.access.label")}
@ -366,198 +360,28 @@ const Edit = () => {
</FormItem> </FormItem>
)} )}
/> />
{/* 超时时间 */}
<FormField <FormField
control={form.control} control={form.control}
name="targetType" name="timeout"
render={({ field }) => ( render={({ field }) => (
<FormItem hidden={tab != "base"}> <FormItem>
<FormLabel> <FormLabel>{t("timeout")}</FormLabel>
{t("domain.management.edit.target.type")}
</FormLabel>
<FormControl> <FormControl>
<Select <Input
{...field} type="number"
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t( placeholder={t(
"domain.management.edit.target.type.not.empty.message" "domain.management.edit.timeout.placeholder"
)} )}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t("domain.management.edit.target.type")}
</SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>
{t(targetTypeMap.get(key)?.[0] || "")}
</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetAccess"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="w-full flex justify-between">
<div>{t("domain.management.edit.target.access")}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("add")}
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
form.setValue("targetAccess", value);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"domain.management.edit.target.access.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t(
"domain.management.edit.target.access.content.label"
)}{" "}
{form.getValues().targetAccess}
</SelectLabel>
<SelectItem value="emptyId">
<div className="flex items-center space-x-2">
--
</div>
</SelectItem>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(item.configType)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
<FormLabel className="w-full flex justify-between">
<div>{t("domain.management.edit.group.label")}</div>
</FormLabel>
<FormControl>
<Select
{...field} {...field}
value={field.value} value={field.value}
defaultValue="emptyId" onChange={(e) => {
onValueChange={(value) => { form.setValue(
form.setValue("group", value); "timeout",
}} parseInt(e.target.value)
>
<SelectTrigger>
<SelectValue
placeholder={t(
"domain.management.edit.group.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
--
</div>
</SelectItem>
{accessGroups
.filter((item) => {
return (
item.expand && item.expand?.access.length > 0
); );
}) }}
.map((item) => (
<SelectItem
value={item.id ? item.id : ""}
key={item.id}
>
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
{item.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="variables"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel>{t("variables")}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
"domain.management.edit.variables.placeholder"
)}
{...field}
className="placeholder:whitespace-pre-wrap"
/> />
</FormControl> </FormControl>
@ -566,21 +390,19 @@ const Edit = () => {
)} )}
/> />
{/* nameservers */}
<FormField <FormField
control={form.control} control={form.control}
name="nameservers" name="nameservers"
render={({ field }) => ( render={({ field }) => (
<FormItem hidden={tab != "advance"}> <FormItem>
<FormLabel>{t("dns")}</FormLabel> <StringList
<FormControl> value={field.value ?? ""}
<Textarea onValueChange={(val: string) => {
placeholder={t( form.setValue("nameservers", val);
"domain.management.edit.dns.placeholder" }}
)} valueType="dns"
{...field} ></StringList>
className="placeholder:whitespace-pre-wrap"
/>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -588,11 +410,38 @@ const Edit = () => {
/> />
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit">{t("save")}</Button> <Button type="submit">
{domain?.id ? t("save") : t("next")}
</Button>
</div> </div>
</form> </form>
</Form> </Form>
</div> </div>
<div
className={cn(
"flex flex-col space-y-5",
tab == "apply" && "hidden"
)}
>
<div className="flex justify-end py-2 border-b">
<Button size={"sm"} variant={"secondary"}>
</Button>
</div>
<div className="w-full md:w-[35em] rounded mt-5 border">
<div className="">
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
<div>ALIYUN-CDN</div>
<div className="flex space-x-2">
<EditIcon size={16} />
<Trash2 size={16} />
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</> </>

View File

@ -97,6 +97,7 @@ const Home = () => {
const checkedDomains = domains.filter((domain) => domain.id === id); const checkedDomains = domains.filter((domain) => domain.id === id);
const isChecked = checkedDomains[0].enabled; const isChecked = checkedDomains[0].enabled;
const data = checkedDomains[0]; const data = checkedDomains[0];
data.enabled = !isChecked; data.enabled = !isChecked;