refactor: divide DeployList

This commit is contained in:
Fu Diwei 2024-10-22 17:44:39 +08:00
parent e53749e16e
commit 062d66222a
13 changed files with 1030 additions and 747 deletions

View File

@ -36,10 +36,10 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
const [configType, setConfigType] = useState(data?.configType || "");
let form = <> </>;
let childComponent = <> </>;
switch (configType) {
case "aliyun":
form = (
childComponent = (
<AccessAliyunForm
data={data}
op={op}
@ -50,7 +50,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "tencent":
form = (
childComponent = (
<AccessTencentForm
data={data}
op={op}
@ -61,7 +61,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "huaweicloud":
form = (
childComponent = (
<AccessHuaweiCloudForm
data={data}
op={op}
@ -72,7 +72,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "qiniu":
form = (
childComponent = (
<AccessQiniuForm
data={data}
op={op}
@ -83,7 +83,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "aws":
form = (
childComponent = (
<AccessAwsForm
data={data}
op={op}
@ -94,7 +94,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "cloudflare":
form = (
childComponent = (
<AccessCloudflareForm
data={data}
op={op}
@ -105,7 +105,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "namesilo":
form = (
childComponent = (
<AccessNamesiloForm
data={data}
op={op}
@ -116,7 +116,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "godaddy":
form = (
childComponent = (
<AccessGodaddyForm
data={data}
op={op}
@ -127,7 +127,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "pdns":
form = (
childComponent = (
<AccessPdnsForm
data={data}
op={op}
@ -138,7 +138,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "httpreq":
form = (
childComponent = (
<AccessHttpreqForm
data={data}
op={op}
@ -149,7 +149,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "local":
form = (
childComponent = (
<AccessLocalForm
data={data}
op={op}
@ -160,7 +160,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "ssh":
form = (
childComponent = (
<AccessSSHForm
data={data}
op={op}
@ -171,7 +171,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "webhook":
form = (
childComponent = (
<AccessWebhookForm
data={data}
op={op}
@ -182,7 +182,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
);
break;
case "k8s":
form = (
childComponent = (
<AccessKubernetesForm
data={data}
op={op}
@ -244,7 +244,7 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
</Select>
</div>
<div className="mt-8">{form}</div>
<div className="mt-8">{childComponent}</div>
</div>
</ScrollArea>
</DialogContent>

View File

@ -0,0 +1,16 @@
import { createContext, useContext } from "react";
import { DeployConfig } from "@/domain/domain";
type DeployEditContext = {
deploy: DeployConfig;
error: Record<string, string>;
setDeploy: (deploy: DeployConfig) => void;
setError: (error: Record<string, string>) => void;
};
export const Context = createContext<DeployEditContext>({} as DeployEditContext);
export const useDeployEditContext = () => {
return useContext(Context);
};

View File

@ -0,0 +1,252 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import AccessEditDialog from "./AccessEditDialog";
import { Context as DeployEditContext } from "./DeployEdit";
import DeployToAliyunOSS from "./DeployToAliyunOSS";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { useConfigContext } from "@/providers/config";
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
const { t } = useTranslation();
const {
config: { accesses },
} = useConfigContext();
const [deployType, setDeployType] = useState("");
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
access: "",
type: "",
});
const [error, setError] = useState<Record<string, string>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
} else {
setLocDeployConfig({
access: "",
type: "",
});
}
}, [deployConfig]);
useEffect(() => {
setDeployType(locDeployConfig.type);
setError({});
}, [locDeployConfig.type]);
const setDeploy = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
} else {
setLocDeployConfig({ ...deploy });
}
},
[locDeployConfig.type]
);
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (locDeployConfig.type == "") {
return true;
}
return item.configType === locDeployConfig.type.split("-")[0];
});
const handleSaveClick = () => {
// 验证数据
const newError = { ...error };
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
setError(newError);
if (Object.values(newError).some((e) => !!e)) return;
// 保存数据
onSave(locDeployConfig);
// 清理数据
setLocDeployConfig({
access: "",
type: "",
});
setError({});
// 关闭弹框
setOpen(false);
};
let childComponent = <></>;
switch (deployType) {
case "aliyun-oss":
childComponent = <DeployToAliyunOSS />;
break;
case "aliyun-cdn":
case "aliyun-dcdn":
childComponent = <DeployToAliyunCDN />;
break;
case "tencent-cdn":
childComponent = <DeployToTencentCDN />;
break;
case "huaweicloud-cdn":
childComponent = <DeployToHuaweiCloudCDN />;
break;
case "qiniu-cdn":
childComponent = <DeployToQiniuCDN />;
break;
case "ssh":
case "local":
childComponent = <DeployToSSH />;
break;
case "webhook":
childComponent = <DeployToWebhook />;
break;
case "k8s-secret":
childComponent = <DeployToKubernetesSecret />;
break;
}
return (
<DeployEditContext.Provider
value={{
deploy: locDeployConfig,
error: error,
setDeploy: setDeploy,
setError: setError,
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="dark:text-stone-200">
<DialogHeader>
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img className="w-6" src={target.icon} />
<div>{t(target.name)}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.type}</div>
</div>
{/* 授权配置 */}
<div className="mt-8">
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.access}</div>
</div>
{/* 其他参数 */}
<div className="mt-8">{childComponent}</div>
</div>
</ScrollArea>
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
export default DeployEditDialog;

View File

@ -1,37 +1,16 @@
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { EditIcon, Plus, Trash2 } from "lucide-react";
import { EditIcon, Trash2 } from "lucide-react";
import Show from "@/components/Show";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import AccessEditDialog from "./AccessEditDialog";
import KVList from "./KVList";
import { DeployConfig, KVType, targetTypeKeys, targetTypeMap } from "@/domain/domain";
import DeployEditDialog from "./DeployEditDialog";
import { DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { useConfigContext } from "@/providers/config";
type DeployEditContextProps = {
deploy: DeployConfig;
error: Record<string, string>;
setDeploy: (deploy: DeployConfig) => void;
setError: (error: Record<string, string>) => void;
};
const DeployEditContext = createContext<DeployEditContextProps>({} as DeployEditContextProps);
export const useDeployEditContext = () => {
return useContext(DeployEditContext);
};
type DeployItemProps = {
item: DeployConfig;
onDelete: () => void;
@ -190,693 +169,3 @@ const DeployList = ({ deploys, onChange }: DeployListProps) => {
};
export default DeployList;
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
const {
config: { accesses },
} = useConfigContext();
const [deployType, setDeployType] = useState<TargetType>();
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
access: "",
type: "",
});
const [error, setError] = useState<Record<string, string>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
} else {
setLocDeployConfig({
access: "",
type: "",
});
}
}, [deployConfig]);
useEffect(() => {
const temp = locDeployConfig.type.split("-");
let t;
if (temp && temp.length > 1) {
// TODO: code smell, maybe a dictionary is better
t = temp[0] === "k8s" ? temp[0] : temp[1];
} else {
t = locDeployConfig.type;
}
setDeployType(t as TargetType);
setError({});
}, [locDeployConfig.type]);
const setDeploy = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
} else {
setLocDeployConfig({ ...deploy });
}
},
[locDeployConfig.type]
);
const { t } = useTranslation();
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (locDeployConfig.type == "") {
return true;
}
const types = locDeployConfig.type.split("-");
return item.configType === types[0];
});
const handleSaveClick = () => {
// 验证数据
// 保存数据
// 清理数据
// 关闭弹框
const newError = { ...error };
if (locDeployConfig.type === "") {
newError.type = t("domain.deployment.form.access.placeholder");
} else {
newError.type = "";
}
if (locDeployConfig.access === "") {
newError.access = t("domain.deployment.form.access.placeholder");
} else {
newError.access = "";
}
setError(newError);
for (const key in newError) {
if (newError[key] !== "") {
return;
}
}
onSave(locDeployConfig);
setLocDeployConfig({
access: "",
type: "",
});
setError({});
setOpen(false);
};
return (
<DeployEditContext.Provider
value={{
deploy: locDeployConfig,
error: error,
setDeploy: setDeploy,
setError: setError,
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="dark:text-stone-200">
<DialogHeader>
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
{targetTypeKeys.map((item) => (
<SelectItem key={item} value={item}>
<div className="flex items-center space-x-2">
<img className="w-6" src={targetTypeMap.get(item)?.[1]} />
<div>{t(targetTypeMap.get(item)?.[0] ?? "")}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.type}</div>
</div>
{/* 授权配置 */}
<div>
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.access}</div>
</div>
{/* 其他参数 */}
<DeployEdit type={deployType!} />
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
type TargetType = "oss" | "cdn" | "dcdn" | "local" | "ssh" | "webhook" | "k8s";
type DeployEditProps = {
type: TargetType;
};
const DeployEdit = ({ type }: DeployEditProps) => {
const getDeploy = () => {
switch (type) {
case "cdn":
return <DeployToCDN />;
case "dcdn":
return <DeployToCDN />;
case "oss":
return <DeployToOSS />;
case "ssh":
return <DeployToSSH />;
case "local":
return <DeployToSSH />;
case "webhook":
return <DeployToWebhook />;
case "k8s":
return <DeployToKubernetes />;
default:
return <DeployToCDN />;
}
};
return getDeploy();
};
const DeployToSSH = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
preCommand: "",
command: "sudo service nginx reload",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.ssh_cert_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.ssh_cert_path.label")}
className="w-full mt-1"
value={data?.config?.certPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.certPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.ssh_key_path.placeholder")}
className="w-full mt-1"
value={data?.config?.keyPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.keyPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
placeholder={t("domain.deployment.form.ssh_pre_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.preCommand = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.command}
placeholder={t("domain.deployment.form.ssh_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.command = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
</div>
</>
);
};
const DeployToWebhook = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
return (
<>
<KVList
variables={data?.config?.variables}
onValueChange={(variables: KVType[]) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.variables = variables;
});
setDeploy(newData);
}}
/>
</>
);
};
const DeployToOSS = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const bucketResp = bucketSchema.safeParse(data.config?.domain);
if (!bucketResp.success) {
setError({
...error,
bucket: JSON.parse(bucketResp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
}, []);
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
endpoint: "oss-cn-hangzhou.aliyuncs.com",
bucket: "",
domain: "",
},
});
}
}, []);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
const bucketSchema = z.string().min(1, {
message: t("domain.deployment.form.oss_bucket.placeholder"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.oss_endpoint.label")}</Label>
<Input
placeholder={t("domain.deployment.form.oss_endpoint.placeholder")}
className="w-full mt-1"
value={data?.config?.endpoint}
onChange={(e) => {
const temp = e.target.value;
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.endpoint = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.endpoint}</div>
<Label>{t("domain.deployment.form.oss_bucket.label")}</Label>
<Input
placeholder={t("domain.deployment.form.oss_bucket.placeholder")}
className="w-full mt-1"
value={data?.config?.bucket}
onChange={(e) => {
const temp = e.target.value;
const resp = bucketSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.bucket = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.bucket}</div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.label")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
const DeployToCDN = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
const DeployToKubernetes = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
namespace: "default",
secretName: "",
secretDataKeyForCrt: "tls.crt",
secretDataKeyForKey: "tls.key",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.k8s_namespace.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_namespace.label")}
className="w-full mt-1"
value={data?.config?.namespace}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.namespace = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_name.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_name.label")}
className="w-full mt-1"
value={data?.config?.secretName}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretName = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForCrt}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForCrt = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_key.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_key.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForKey}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForKey = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,77 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToAliyunCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
export default DeployToAliyunCDN;

View File

@ -0,0 +1,164 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToAliyunOSS = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const bucketResp = bucketSchema.safeParse(data.config?.domain);
if (!bucketResp.success) {
setError({
...error,
bucket: JSON.parse(bucketResp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
}, []);
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
endpoint: "oss-cn-hangzhou.aliyuncs.com",
bucket: "",
domain: "",
},
});
}
}, []);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
const bucketSchema = z.string().min(1, {
message: t("domain.deployment.form.oss_bucket.placeholder"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.oss_endpoint.label")}</Label>
<Input
placeholder={t("domain.deployment.form.oss_endpoint.placeholder")}
className="w-full mt-1"
value={data?.config?.endpoint}
onChange={(e) => {
const temp = e.target.value;
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.endpoint = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.endpoint}</div>
</div>
<div>
<Label>{t("domain.deployment.form.oss_bucket.label")}</Label>
<Input
placeholder={t("domain.deployment.form.oss_bucket.placeholder")}
className="w-full mt-1"
value={data?.config?.bucket}
onChange={(e) => {
const temp = e.target.value;
const resp = bucketSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.bucket = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.bucket}</div>
</div>
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.label")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
export default DeployToAliyunOSS;

View File

@ -0,0 +1,77 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToHuaweiCloudCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
export default DeployToHuaweiCloudCDN;

View File

@ -0,0 +1,104 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToKubernetesSecret = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
namespace: "default",
secretName: "",
secretDataKeyForCrt: "tls.crt",
secretDataKeyForKey: "tls.key",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.k8s_namespace.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_namespace.label")}
className="w-full mt-1"
value={data?.config?.namespace}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.namespace = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_name.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_name.label")}
className="w-full mt-1"
value={data?.config?.secretName}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretName = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForCrt}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForCrt = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_key.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_key.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForKey}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForKey = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
</div>
</>
);
};
export default DeployToKubernetesSecret;

View File

@ -0,0 +1,77 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToQiniuCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
export default DeployToQiniuCDN;

View File

@ -0,0 +1,113 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useDeployEditContext } from "./DeployEdit";
const DeployToSSH = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
preCommand: "",
command: "sudo service nginx reload",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.ssh_cert_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.ssh_cert_path.label")}
className="w-full mt-1"
value={data?.config?.certPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.certPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.ssh_key_path.placeholder")}
className="w-full mt-1"
value={data?.config?.keyPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.keyPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
placeholder={t("domain.deployment.form.ssh_pre_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.preCommand = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
<div>
<Label>{t("domain.deployment.form.ssh_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.command}
placeholder={t("domain.deployment.form.ssh_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.command = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
</div>
</>
);
};
export default DeployToSSH;

View File

@ -0,0 +1,77 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToTencentCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
export default DeployToTencentCDN;

View File

@ -0,0 +1,35 @@
import { useEffect } from "react";
import { produce } from "immer";
import { useDeployEditContext } from "./DeployEdit";
import KVList from "./KVList";
import { type KVType } from "@/domain/domain";
const DeployToWebhook = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
return (
<>
<KVList
variables={data?.config?.variables}
onValueChange={(variables: KVType[]) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.variables = variables;
});
setDeploy(newData);
}}
/>
</>
);
};
export default DeployToWebhook;

View File

@ -63,21 +63,23 @@ export type Statistic = {
disabled: number;
};
export const getLastDeployment = (domain: Domain): Deployment | undefined => {
return domain.expand?.lastDeployment;
type DeployTarget = {
type: string;
name: string;
icon: string;
};
export const targetTypeMap: Map<string, [string, string]> = new Map([
["aliyun-oss", ["common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"]],
["aliyun-cdn", ["common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["common.provider.tencent.cdn", "/imgs/providers/tencent.svg"]],
["huaweicloud-cdn", ["common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"]],
["qiniu-cdn", ["common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"]],
["local", ["common.provider.local", "/imgs/providers/local.svg"]],
["ssh", ["common.provider.ssh", "/imgs/providers/ssh.svg"]],
["webhook", ["common.provider.webhook", "/imgs/providers/webhook.svg"]],
["k8s-secret", ["common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"]],
]);
export const targetTypeKeys = Array.from(targetTypeMap.keys());
export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map(
[
["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"],
["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"],
["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"],
["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"],
["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"],
["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"],
["local", "common.provider.local", "/imgs/providers/local.svg"],
["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"],
["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"],
["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"],
].map(([type, name, icon]) => [type, { type, name, icon }])
);