feat(ui): new DeployNodeForm using antd

This commit is contained in:
Fu Diwei
2024-12-31 19:55:34 +08:00
parent cb7a465d6c
commit 6f088fd76a
53 changed files with 2808 additions and 285 deletions

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from "react";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "../ui/select";
import { accessProvidersMap } from "@/domain/access";
import { accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { useTranslation } from "react-i18next";
import { useAccessStore } from "@/stores/access";
import { deployTargetsMap } from "@/domain/domain";
type AccessSelectProps = {
providerType: string;
@@ -24,8 +23,7 @@ const AccessSelect = ({ value, onValueChange, providerType }: AccessSelectProps)
}, [value]);
const targetAccesses = accesses.filter((item) => {
console.log(item, providerType);
return item.configType === deployTargetsMap.get(providerType)?.provider;
return item.configType === deployProvidersMap.get(providerType)?.provider;
});
return (

View File

@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { Dropdown } from "antd";
import { Plus as PlusIcon } from "lucide-react";
import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import { useZustandShallowSelector } from "@/hooks";
import { newWorkflowNode, workflowNodeDropdownList, WorkflowNodeType } from "@/domain/workflow";
@@ -22,7 +22,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
};
return (
<div className="before:content-[''] before:w-[2px] before:bg-stone-200 before:absolute before:h-full before:left-[50%] before:-translate-x-[50%] before:top-0 pt-6 pb-9 relative flex flex-col items-center">
<div className="before:content-[''] before:w-[2px] before:bg-stone-200 before:absolute before:h-full before:left-[50%] before:-translate-x-[50%] before:top-0 py-6 relative">
<Dropdown
menu={{
items: workflowNodeDropdownList.map((item) => {
@@ -56,8 +56,8 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
}}
trigger={["click"]}
>
<div className="bg-stone-400 hover:bg-stone-500 rounded-full z-10 relative outline-none">
<PlusIcon className="text-white" size={18} />
<div className="bg-stone-400 hover:bg-stone-500 rounded-full size-5 z-[1] relative flex items-center justify-center cursor-pointer">
<PlusOutlinedIcon className="text-white" />
</div>
</Dropdown>
</div>

View File

@@ -32,7 +32,7 @@ const BranchNode = memo(({ data }: BrandNodeProps) => {
}}
size={"sm"}
variant={"outline"}
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-10 dark:text-stone-200"
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-[1] dark:text-stone-200"
>
{t("workflow.node.addBranch.label")}
</Button>

View File

@@ -1,9 +1,10 @@
import { useWorkflowStore } from "@/stores/workflow";
import AddNode from "./AddNode";
import { NodeProps } from "./types";
import { useZustandShallowSelector } from "@/hooks";
import { Dropdown } from "antd";
import { Ellipsis, Trash2 } from "lucide-react";
import { DeleteOutlined as DeleteOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon } from "@ant-design/icons";
import AddNode from "./AddNode";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import { type NodeProps } from "./types";
const ConditionNode = ({ data, branchId, branchIndex }: NodeProps) => {
const { updateNode, removeBranch } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeBranch"]));
@@ -12,14 +13,14 @@ const ConditionNode = ({ data, branchId, branchIndex }: NodeProps) => {
};
return (
<>
<div className="rounded-md shadow-md w-[261px] mt-10 relative z-10">
<div className="rounded-md shadow-md w-[261px] mt-10 relative z-[1]">
<Dropdown
menu={{
items: [
{
key: "delete",
label: "删除分支",
icon: <Trash2 size={16} />,
icon: <DeleteOutlinedIcon />,
danger: true,
onClick: () => {
removeBranch(branchId ?? "", branchIndex ?? 0);
@@ -30,7 +31,7 @@ const ConditionNode = ({ data, branchId, branchIndex }: NodeProps) => {
trigger={["click"]}
>
<div className="absolute right-2 top-1 cursor-pointer">
<Ellipsis size={17} className="text-stone-600" />
<EllipsisOutlinedIcon size={17} className="text-stone-600" />
</div>
</Dropdown>

View File

@@ -1,31 +1,33 @@
import { WorkflowNode } from "@/domain/workflow";
import { memo } from "react";
import DeployToAliyunOSS from "./DeployToAliyunOss";
import { type WorkflowNode } from "@/domain/workflow";
import DeployToAliyunALB from "./DeployToAliyunALB";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToAliyunCLB from "./DeployToAliyunCLB";
import DeployToAliyunNLB from "./DeployToAliyunNLB";
import DeployToAliyunOSS from "./DeployToAliyunOss";
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
import DeployToBytePlusCDN from "./DeployToByteplusCDN";
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
import DeployToLocal from "./DeployToLocal";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToWebhook from "./DeployToWebhook";
import DeployToSSH from "./DeployToSSH";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToTencentCLB from "./DeployToTencentCLB";
import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToSSH from "./DeployToSSH";
import DeployToLocal from "./DeployToLocal";
import DeployToByteplusCDN from "./DeployToByteplusCDN";
import DeployToVolcengineCDN from "./DeployToVolcengineCDN";
import DeployToVolcengineLive from "./DeployToVolcengineLive";
import DeployToTencentEO from "./DeployToTencentTEO";
import DeployToVolcEngineCDN from "./DeployToVolcengineCDN";
import DeployToVolcEngineLive from "./DeployToVolcengineLive";
import DeployToWebhook from "./DeployToWebhook";
export type DeployFormProps = {
data: WorkflowNode;
defaultProivder?: string;
};
const DeployForm = ({ data, defaultProivder }: DeployFormProps) => {
return <div className="dark:text-stone-200">{getForm(data, defaultProivder)}</div>;
};
@@ -68,17 +70,17 @@ const getForm = (data: WorkflowNode, defaultProivder?: string) => {
case "tencentcloud-cos":
return <DeployToTencentCOS data={data} />;
case "tencentcloud-eo":
return <DeployToTencentTEO data={data} />;
return <DeployToTencentEO data={data} />;
case "ssh":
return <DeployToSSH data={data} />;
case "local":
return <DeployToLocal data={data} />;
case "byteplus-cdn":
return <DeployToByteplusCDN data={data} />;
return <DeployToBytePlusCDN data={data} />;
case "volcengine-cdn":
return <DeployToVolcengineCDN data={data} />;
return <DeployToVolcEngineCDN data={data} />;
case "volcengine-live":
return <DeployToVolcengineLive data={data} />;
return <DeployToVolcEngineLive data={data} />;
default:
return <></>;
}

View File

@@ -1,13 +1,15 @@
import { WorkflowNode } from "@/domain/workflow";
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Show from "../Show";
import DeployForm from "./DeployForm";
import { DeployTarget, deployTargets } from "@/domain/domain";
import Show from "@/components/Show";
import DeployNodeForm from "./node/DeployNodeForm";
import { deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode } from "@/domain/workflow";
type DeployPanelBodyProps = {
data: WorkflowNode;
};
const DeployPanelBody = ({ data }: DeployPanelBodyProps) => {
const { t } = useTranslation();
@@ -21,33 +23,22 @@ const DeployPanelBody = ({ data }: DeployPanelBodyProps) => {
return (
<>
{/* 默认展示服务商列表 */}
<Show when={!providerType} fallback={<DeployForm data={data} defaultProivder={providerType} />}>
<Show when={!providerType} fallback={<DeployNodeForm data={data} defaultProivderType={providerType} />}>
<div className="text-lg font-semibold text-gray-700"></div>
{deployTargets
.reduce((acc: DeployTarget[][], provider, index) => {
if (index % 2 === 0) {
acc.push([provider]);
} else {
acc[acc.length - 1].push(provider);
}
return acc;
}, [])
.map((providerRow, rowIndex) => (
<div key={rowIndex} className="flex space-x-5">
{providerRow.map((provider, index) => (
<div
key={index}
className="flex space-x-2 w-[47%] items-center cursor-pointer hover:bg-slate-100 p-2 rounded-sm"
onClick={() => {
setProviderType(provider.type);
}}
>
<img src={provider.icon} alt={provider.type} className="w-8 h-8" />
<div className="text-muted-foreground text-sm">{t(provider.name)}</div>
</div>
))}
{Array.from(deployProvidersMap.values()).map((provider, index) => {
return (
<div
key={index}
className="flex space-x-2 w-[47%] items-center cursor-pointer hover:bg-slate-100 p-2 rounded-sm"
onClick={() => {
setProviderType(provider.type);
}}
>
<img src={provider.icon} alt={provider.type} className="w-8 h-8" />
<div className="text-muted-foreground text-sm">{t(provider.name)}</div>
</div>
))}
);
})}
</Show>
</>
);

View File

@@ -1,15 +1,16 @@
import { WorkflowNode, WorkflowNodeType } from "@/domain/workflow";
import AddNode from "./AddNode";
import { useWorkflowStore } from "@/stores/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useTranslation } from "react-i18next";
import { Dropdown } from "antd";
import { Ellipsis, Trash2 } from "lucide-react";
import { DeleteOutlined as DeleteOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon } from "@ant-design/icons";
import Show from "@/components/Show";
import AddNode from "./AddNode";
import { usePanel } from "./PanelProvider";
import PanelBody from "./PanelBody";
import { useTranslation } from "react-i18next";
import Show from "../Show";
import { deployTargetsMap } from "@/domain/domain";
import { useZustandShallowSelector } from "@/hooks";
import { deployProvidersMap } from "@/domain/provider";
import { notifyChannelsMap } from "@/domain/settings";
import { type WorkflowNode, WorkflowNodeType } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow";
type NodeProps = {
data: WorkflowNode;
@@ -56,7 +57,7 @@ const Node = ({ data }: NodeProps) => {
case WorkflowNodeType.Apply:
return <div className="text-muted-foreground truncate">{data.config?.domain as string}</div>;
case WorkflowNodeType.Deploy: {
const provider = deployTargetsMap.get(data.config?.providerType as string);
const provider = deployProvidersMap.get(data.config?.providerType as string);
return (
<div className="flex space-x-2 items-center text-muted-foreground">
<img src={provider?.icon} className="w-6 h-6" />
@@ -90,7 +91,7 @@ const Node = ({ data }: NodeProps) => {
{
key: "delete",
label: t(`${i18nPrefix}.delete.label`),
icon: <Trash2 size={16} />,
icon: <DeleteOutlinedIcon />,
danger: true,
onClick: () => {
removeNode(data.id);
@@ -101,7 +102,7 @@ const Node = ({ data }: NodeProps) => {
trigger={["click"]}
>
<div className="absolute right-2 top-1 cursor-pointer">
<Ellipsis className="text-white" size={17} />
<EllipsisOutlinedIcon className="text-white" size={17} />
</div>
</Dropdown>
</>

View File

@@ -4,6 +4,7 @@ import { useControllableValue } from "ahooks";
import { AutoComplete, Button, Divider, Form, Input, Select, Space, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { produce } from "immer";
import z from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
@@ -12,7 +13,8 @@ import ModalForm from "@/components/core/ModalForm";
import MultipleInput from "@/components/core/MultipleInput";
import { usePanel } from "../PanelProvider";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { ACCESS_USAGES, accessProvidersMap } from "@/domain/access";
import { ACCESS_USAGES } from "@/domain/access";
import { accessProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow";
import { useContactStore } from "@/stores/contact";
import { useWorkflowStore } from "@/stores/workflow";
@@ -42,33 +44,27 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
const { hidePanel } = usePanel();
const formSchema = z.object({
domain: z.string({ message: t("workflow.nodes.apply.form.domains.placeholder") }).refine(
(v) => {
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validDomainName(e, true));
},
{ message: t("common.errmsg.domain_invalid") }
),
email: z.string({ message: t("workflow.nodes.apply.form.email.placeholder") }).email("common.errmsg.email_invalid"),
access: z.string({ message: t("workflow.nodes.apply.form.access.placeholder") }).min(1, t("workflow.nodes.apply.form.access.placeholder")),
domain: z.string({ message: t("workflow_node.apply.form.domains.placeholder") }).refine((v) => {
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validDomainName(e, true));
}, t("common.errmsg.domain_invalid")),
email: z.string({ message: t("workflow_node.apply.form.email.placeholder") }).email("common.errmsg.email_invalid"),
access: z.string({ message: t("workflow_node.apply.form.access.placeholder") }).min(1, t("workflow_node.apply.form.access.placeholder")),
keyAlgorithm: z.string().nullish(),
nameservers: z
.string()
.refine(
(v) => {
if (!v) return true;
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
},
{ message: t("common.errmsg.host_invalid") }
)
.nullish(),
.nullish()
.refine((v) => {
if (!v) return true;
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
}, t("common.errmsg.host_invalid")),
propagationTimeout: z
.union([
z.number().int().gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder")),
z.string().refine((v) => !v || (parseInt(v) === +v && +v > 0), { message: t("workflow.nodes.apply.form.propagation_timeout.placeholder") }),
z.number().int().gte(1, t("workflow_node.apply.form.propagation_timeout.placeholder")),
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.propagation_timeout.placeholder")),
])
.nullish(),
disableFollowCNAME: z.boolean().nullish(),
@@ -81,8 +77,14 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
} = useAntdForm<z.infer<typeof formSchema>>({
initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => {
await updateNode({ ...data, config: { ...values }, validated: true });
await formInst.validateFields();
await addEmail(values.email);
await updateNode(
produce(data, (draft) => {
draft.config = { ...values };
draft.validated = true;
})
);
hidePanel();
},
});
@@ -106,15 +108,15 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
<Form {...formProps} form={formInst} disabled={formPending} layout="vertical">
<Form.Item
name="domain"
label={t("workflow.nodes.apply.form.domains.label")}
label={t("workflow_node.apply.form.domains.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.domains.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.domains.tooltip") }}></span>}
>
<Space.Compact style={{ width: "100%" }}>
<Input
disabled={formPending}
value={fieldDomains}
placeholder={t("workflow.nodes.apply.form.domains.placeholder")}
placeholder={t("workflow_node.apply.form.domains.placeholder")}
onChange={handleFieldDomainsChange}
/>
<FormFieldDomainsModalForm
@@ -134,19 +136,19 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
<Form.Item
name="email"
label={t("workflow.nodes.apply.form.email.label")}
label={t("workflow_node.apply.form.email.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.email.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.email.tooltip") }}></span>}
>
<FormFieldEmailSelect placeholder={t("workflow.nodes.apply.form.email.placeholder")} />
<FormFieldEmailSelect placeholder={t("workflow_node.apply.form.email.placeholder")} />
</Form.Item>
<Form.Item>
<Form.Item className="mb-0">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">
<span>{t("workflow.nodes.apply.form.access.label")}</span>
<Tooltip title={t("workflow.nodes.apply.form.access.tooltip")}>
<span>{t("workflow_node.apply.form.access.label")}</span>
<Tooltip title={t("workflow_node.apply.form.access.tooltip")}>
<Typography.Text className="ms-1" type="secondary">
<QuestionCircleOutlinedIcon />
</Typography.Text>
@@ -158,7 +160,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow.nodes.apply.form.access.button")}
{t("workflow_node.apply.form.access.button")}
</Button>
}
onSubmit={(record) => {
@@ -173,7 +175,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</label>
<Form.Item name="access" rules={[formRule]}>
<AccessSelect
placeholder={t("workflow.nodes.apply.form.access.placeholder")}
placeholder={t("workflow_node.apply.form.access.placeholder")}
filter={(record) => {
const provider = accessProvidersMap.get(record.configType);
return ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage;
@@ -183,33 +185,33 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</Form.Item>
<Divider className="my-1">
<Typography.Text className="text-xs" type="secondary">
{t("workflow.nodes.apply.form.advanced_settings.label")}
<Typography.Text className="font-normal text-xs" type="secondary">
{t("workflow_node.apply.form.advanced_config.label")}
</Typography.Text>
</Divider>
<Form.Item name="keyAlgorithm" label={t("workflow.nodes.apply.form.key_algorithm.label")} rules={[formRule]}>
<Form.Item name="keyAlgorithm" label={t("workflow_node.apply.form.key_algorithm.label")} rules={[formRule]}>
<Select
options={["RSA2048", "RSA3072", "RSA4096", "RSA8192", "EC256", "EC384"].map((e) => ({
label: e,
value: e,
}))}
placeholder={t("workflow.nodes.apply.form.key_algorithm.placeholder")}
placeholder={t("workflow_node.apply.form.key_algorithm.placeholder")}
/>
</Form.Item>
<Form.Item
name="nameservers"
label={t("workflow.nodes.apply.form.nameservers.label")}
label={t("workflow_node.apply.form.nameservers.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.nameservers.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.nameservers.tooltip") }}></span>}
>
<Space.Compact style={{ width: "100%" }}>
<Input
allowClear
disabled={formPending}
value={fieldNameservers}
placeholder={t("workflow.nodes.apply.form.nameservers.placeholder")}
placeholder={t("workflow_node.apply.form.nameservers.placeholder")}
onChange={handleFieldNameserversChange}
/>
<FormFieldNameserversModalForm
@@ -229,25 +231,25 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
<Form.Item
name="propagationTimeout"
label={t("workflow.nodes.apply.form.propagation_timeout.label")}
label={t("workflow_node.apply.form.propagation_timeout.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.propagation_timeout.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.propagation_timeout.tooltip") }}></span>}
>
<Input
type="number"
allowClear
min={0}
max={3600}
placeholder={t("workflow.nodes.apply.form.propagation_timeout.placeholder")}
addonAfter={t("workflow.nodes.apply.form.propagation_timeout.suffix")}
placeholder={t("workflow_node.apply.form.propagation_timeout.placeholder")}
addonAfter={t("workflow_node.apply.form.propagation_timeout.suffix")}
/>
</Form.Item>
<Form.Item
name="disableFollowCNAME"
label={t("workflow.nodes.apply.form.disable_follow_cname.label")}
label={t("workflow_node.apply.form.disable_follow_cname.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.disable_follow_cname.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.disable_follow_cname.tooltip") }}></span>}
>
<Switch />
</Form.Item>
@@ -339,12 +341,9 @@ const FormFieldDomainsModalForm = ({
const { t } = useTranslation();
const formSchema = z.object({
domains: z.array(z.string()).refine(
(v) => {
return v.every((e) => !e?.trim() || validDomainName(e.trim(), true));
},
{ message: t("common.errmsg.domain_invalid") }
),
domains: z.array(z.string()).refine((v) => {
return v.every((e) => !e?.trim() || validDomainName(e.trim(), true));
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const [formInst] = Form.useForm<z.infer<typeof formSchema>>();
@@ -372,14 +371,14 @@ const FormFieldDomainsModalForm = ({
form={formInst}
initialValues={model}
modalProps={{ destroyOnClose: true }}
title={t("workflow.nodes.apply.form.domains.multiple_input_modal.title")}
title={t("workflow_node.apply.form.domains.multiple_input_modal.title")}
trigger={trigger}
validateTrigger="onSubmit"
width={480}
onFinish={handleFinish}
>
<Form.Item name="domains" rules={[formRule]}>
<MultipleInput placeholder={t("workflow.nodes.apply.form.domains.multiple_input_modal.placeholder")} />
<MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} />
</Form.Item>
</ModalForm>
);
@@ -389,12 +388,9 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
const { t } = useTranslation();
const formSchema = z.object({
nameservers: z.array(z.string()).refine(
(v) => {
return v.every((e) => !e?.trim() || validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
},
{ message: t("common.errmsg.domain_invalid") }
),
nameservers: z.array(z.string()).refine((v) => {
return v.every((e) => !e?.trim() || validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const [formInst] = Form.useForm<z.infer<typeof formSchema>>();
@@ -422,14 +418,14 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
form={formInst}
initialValues={model}
modalProps={{ destroyOnClose: true }}
title={t("workflow.nodes.apply.form.nameservers.multiple_input_modal.title")}
title={t("workflow_node.apply.form.nameservers.multiple_input_modal.title")}
trigger={trigger}
validateTrigger="onSubmit"
width={480}
onFinish={handleFinish}
>
<Form.Item name="nameservers" rules={[formRule]}>
<MultipleInput placeholder={t("workflow.nodes.apply.form.nameservers.multiple_input_modal.placeholder")} />
<MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} />
</Form.Item>
</ModalForm>
);

View File

@@ -0,0 +1,282 @@
import { memo, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Button, Divider, Form, Select, Space, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { produce } from "immer";
import z from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import { usePanel } from "../PanelProvider";
import DeployNodeFormAliyunALBFields from "./DeployNodeFormAliyunALBFields";
import DeployNodeFormAliyunCLBFields from "./DeployNodeFormAliyunCLBFields";
import DeployNodeFormAliyunCDNFields from "./DeployNodeFormAliyunCDNFields";
import DeployNodeFormAliyunDCDNFields from "./DeployNodeFormAliyunDCDNFields";
import DeployNodeFormAliyunNLBFields from "./DeployNodeFormAliyunNLBFields";
import DeployNodeFormAliyunOSSFields from "./DeployNodeFormAliyunOSSFields";
import DeployNodeFormBaiduCloudCDNFields from "./DeployNodeFormBaiduCloudCDNFields";
import DeployNodeFormBytePlusCDNFields from "./DeployNodeFormBytePlusCDNFields";
import DeployNodeFormDogeCloudCDNFields from "./DeployNodeFormDogeCloudCDNFields";
import DeployNodeFormHuaweiCloudCDNFields from "./DeployNodeFormHuaweiCloudCDNFields";
import DeployNodeFormHuaweiCloudELBFields from "./DeployNodeFormHuaweiCloudELBFields";
import DeployNodeFormKubernetesSecretFields from "./DeployNodeFormKubernetesSecretFields";
import DeployNodeFormLocalFields from "./DeployNodeFormLocalFields";
import DeployNodeFormQiniuCDNFields from "./DeployNodeFormQiniuCDNFields";
import DeployNodeFormSSHFields from "./DeployNodeFormSSHFields";
import DeployNodeFormTencentCloudCDNFields from "./DeployNodeFormTencentCloudCDNFields";
import DeployNodeFormTencentCloudCLBFields from "./DeployNodeFormTencentCloudCLBFields";
import DeployNodeFormTencentCloudCOSFields from "./DeployNodeFormTencentCloudCOSFields";
import DeployNodeFormTencentCloudECDNFields from "./DeployNodeFormTencentCloudECDNFields";
import DeployNodeFormTencentCloudEOFields from "./DeployNodeFormTencentCloudEOFields";
import DeployNodeFormVolcEngineCDNFields from "./DeployNodeFormVolcEngineCDNFields";
import DeployNodeFormVolcEngineLiveFields from "./DeployNodeFormVolcEngineLiveFields";
import DeployNodeFormWebhookFields from "./DeployNodeFormWebhookFields";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { ACCESS_USAGES } from "@/domain/access";
import { accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow";
export type DeployFormProps = {
data: WorkflowNode;
defaultProivderType?: string;
};
const initFormModel = (): WorkflowNodeConfig => {
return {};
};
const DeployNodeForm = ({ data, defaultProivderType }: DeployFormProps) => {
const { t } = useTranslation();
const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
const { hidePanel } = usePanel();
const formSchema = z.object({
providerType: z
.string({ message: t("workflow_node.deploy.form.provider_type.placeholder") })
.nonempty(t("workflow_node.deploy.form.provider_type.placeholder")),
access: z
.string({ message: t("workflow_node.deploy.form.provider_access.placeholder") })
.nonempty(t("workflow_node.deploy.form.provider_access.placeholder")),
certificate: z.string({ message: t("workflow_node.deploy.form.certificate.placeholder") }).nonempty(t("workflow_node.deploy.form.certificate.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const {
form: formInst,
formPending,
formProps,
} = useAntdForm<z.infer<typeof formSchema>>({
initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => {
await formInst.validateFields();
await updateNode(
produce(data, (draft) => {
draft.config = { ...values };
draft.validated = true;
})
);
hidePanel();
},
});
const [previousOutput, setPreviousOutput] = useState<WorkflowNode[]>([]);
useEffect(() => {
const rs = getWorkflowOuptutBeforeId(data.id, "certificate");
setPreviousOutput(rs);
}, [data, getWorkflowOuptutBeforeId]);
const fieldProviderType = Form.useWatch("providerType", formInst);
// const fieldAccess = Form.useWatch("access", formInst);
const formFieldsComponent = useMemo(() => {
/*
注意:如果追加新的子组件,请保持以 ASCII 排序。
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (fieldProviderType) {
case "aliyun-alb":
return <DeployNodeFormAliyunALBFields />;
case "aliyun-clb":
return <DeployNodeFormAliyunCLBFields />;
case "aliyun-cdn":
return <DeployNodeFormAliyunCDNFields />;
case "aliyun-dcdn":
return <DeployNodeFormAliyunDCDNFields />;
case "aliyun-nlb":
return <DeployNodeFormAliyunNLBFields />;
case "aliyun-oss":
return <DeployNodeFormAliyunOSSFields />;
case "baiducloud-cdn":
return <DeployNodeFormBaiduCloudCDNFields />;
case "byteplus-cdn":
return <DeployNodeFormBytePlusCDNFields />;
case "dogecloud-cdn":
return <DeployNodeFormDogeCloudCDNFields />;
case "huaweicloud-cdn":
return <DeployNodeFormHuaweiCloudCDNFields />;
case "huaweicloud-elb":
return <DeployNodeFormHuaweiCloudELBFields />;
case "k8s-secret":
return <DeployNodeFormKubernetesSecretFields />;
case "local":
return <DeployNodeFormLocalFields />;
case "qiniu-cdn":
return <DeployNodeFormQiniuCDNFields />;
case "ssh":
return <DeployNodeFormSSHFields />;
case "tencentcloud-cdn":
return <DeployNodeFormTencentCloudCDNFields />;
case "tencentcloud-clb":
return <DeployNodeFormTencentCloudCLBFields />;
case "tencentcloud-cos":
return <DeployNodeFormTencentCloudCOSFields />;
case "tencentcloud-ecdn":
return <DeployNodeFormTencentCloudECDNFields />;
case "tencentcloud-eo":
return <DeployNodeFormTencentCloudEOFields />;
case "volcengine-cdn":
return <DeployNodeFormVolcEngineCDNFields />;
case "volcengine-live":
return <DeployNodeFormVolcEngineLiveFields />;
case "webhook":
return <DeployNodeFormWebhookFields />;
}
}, [fieldProviderType]);
const handleProviderTypeSelect = (value: string) => {
if (fieldProviderType === value) return;
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
if (data.config?.providerType === value) {
formInst.resetFields();
} else {
const oldValues = formInst.getFieldsValue();
const newValues: Record<string, unknown> = {};
for (const key in oldValues) {
if (key === "providerType" || key === "access" || key === "certificate") {
newValues[key] = oldValues[key];
} else {
newValues[key] = undefined;
}
}
formInst.setFieldsValue(newValues);
}
};
return (
<Form {...formProps} form={formInst} disabled={formPending} layout="vertical">
<Form.Item name="providerType" label={t("workflow_node.deploy.form.provider_type.label")} rules={[formRule]} initialValue={defaultProivderType}>
<Select
showSearch
placeholder={t("workflow_node.deploy.form.provider_type.placeholder")}
filterOption={(searchValue, option) => {
const type = String(option?.value ?? "");
const target = deployProvidersMap.get(type);
const filter = (v?: string) => v?.toLowerCase()?.includes(searchValue.toLowerCase()) ?? false;
return filter(type) || filter(t(target?.name ?? ""));
}}
onSelect={handleProviderTypeSelect}
>
{Array.from(deployProvidersMap.values()).map((item) => {
return (
<Select.Option key={item.type} label={t(item.name)} value={item.type} title={t(item.name)}>
<Space className="flex-grow max-w-full truncate" size={4}>
<Avatar src={item.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(item.name)}
</Typography.Text>
</Space>
</Select.Option>
);
})}
</Select>
</Form.Item>
<Form.Item className="mb-0">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">
<span>{t("workflow_node.deploy.form.provider_access.label")}</span>
<Tooltip title={t("workflow_node.deploy.form.provider_access.tooltip")}>
<Typography.Text className="ms-1" type="secondary">
<QuestionCircleOutlinedIcon />
</Typography.Text>
</Tooltip>
</div>
<div className="text-right">
<AccessEditModal
data={{ configType: deployProvidersMap.get(defaultProivderType!)?.provider }}
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.deploy.form.provider_access.button")}
</Button>
}
onSubmit={(record) => {
const provider = accessProvidersMap.get(record.configType);
if (ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.DEPLOY === provider?.usage) {
formInst.setFieldValue("access", record.id);
}
}}
/>
</div>
</div>
</label>
<Form.Item name="access" rules={[formRule]}>
<AccessSelect
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
filter={(record) => {
if (defaultProivderType) {
return deployProvidersMap.get(defaultProivderType)?.provider === record.configType;
}
const provider = accessProvidersMap.get(record.configType);
return ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage;
}}
/>
</Form.Item>
</Form.Item>
<Form.Item
name="certificate"
label={t("workflow_node.deploy.form.certificate.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.certificate.tooltip") }}></span>}
>
<Select
options={previousOutput.map((item) => {
return {
label: item.name,
options: item.output?.map((output) => {
return {
label: `${item.name} - ${output.label}`,
value: `${item.id}#${output.name}`,
};
}),
};
})}
placeholder={t("workflow_node.deploy.form.certificate.placeholder")}
/>
</Form.Item>
<Divider className="my-1">
<Typography.Text className="font-normal text-xs" type="secondary">
{t("workflow_node.deploy.form.params_config.label")}
</Typography.Text>
</Divider>
{formFieldsComponent}
<Form.Item>
<Button type="primary" htmlType="submit" loading={formPending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
);
};
export default memo(DeployNodeForm);

View File

@@ -0,0 +1,87 @@
import { useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const DeployNodeFormAliyunALBFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.aliyun_alb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.aliyun_alb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.aliyun_alb_region.placeholder"))
.trim(),
loadbalancerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder")),
listenerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_alb_listener_id.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldResourceType = Form.useWatch("resourceType", formInst);
return (
<>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.aliyun_alb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.aliyun_alb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.aliyun_alb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.aliyun_alb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_alb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_alb_region.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerId"
label={t("workflow_node.deploy.form.aliyun_alb_listener_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_alb_listener_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_alb_listener_id.placeholder")} />
</Form.Item>
</Show>
</>
);
};
export default DeployNodeFormAliyunALBFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormAliyunCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.aliyun_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.aliyun_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormAliyunCDNFields;

View File

@@ -0,0 +1,96 @@
import { useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { validPortNumber } from "@/utils/validators";
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const DeployNodeFormAliyunCLBFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.aliyun_clb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.aliyun_clb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.aliyun_clb_region.placeholder"))
.trim(),
loadbalancerId: z
.string({ message: t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder") })
.min(1, t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
listenerPort: z
.union([
z
.number()
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")
),
z
.string()
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")
),
])
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldResourceType = Form.useWatch("resourceType", formInst);
return (
<>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.aliyun_clb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.aliyun_clb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.aliyun_clb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.aliyun_clb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.aliyun_clb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_clb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_clb_region.placeholder")} />
</Form.Item>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerPort"
label={t("workflow_node.deploy.form.aliyun_clb_listener_port.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_clb_listener_port.tooltip") }}></span>}
initialValue={443}
>
<Input type="number" min={1} max={65535} placeholder={t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")} />
</Form.Item>
</Show>
</>
);
};
export default DeployNodeFormAliyunCLBFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormAliyunDCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.aliyun_dcdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.aliyun_dcdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_dcdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_dcdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormAliyunDCDNFields;

View File

@@ -0,0 +1,87 @@
import { useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const DeployNodeFormAliyunNLBFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.aliyun_nlb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.aliyun_nlb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.aliyun_nlb_region.placeholder"))
.trim(),
loadbalancerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder")),
listenerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldResourceType = Form.useWatch("resourceType", formInst);
return (
<>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.aliyun_nlb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.aliyun_nlb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.aliyun_nlb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.aliyun_nlb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.aliyun_nlb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_nlb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_nlb_region.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerId"
label={t("workflow_node.deploy.form.aliyun_nlb_listener_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder")} />
</Form.Item>
</Show>
</>
);
};
export default DeployNodeFormAliyunNLBFields;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormAliyunOSSFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
endpoint: z
.string({ message: t("workflow_node.deploy.form.aliyun_oss_endpoint.placeholder") })
.url(t("common.errmsg.url_invalid"))
.trim(),
bucket: z
.string({ message: t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder") })
.nonempty(t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder"))
.trim(),
domain: z
.string({ message: t("workflow_node.deploy.form.aliyun_oss_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="endpoint"
label={t("workflow_node.deploy.form.aliyun_oss_endpoint.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_oss_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_oss_endpoint.placeholder")} />
</Form.Item>
<Form.Item
name="bucket"
label={t("workflow_node.deploy.form.aliyun_oss_bucket.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_oss_bucket.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder")} />
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.aliyun_oss_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_oss_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aliyun_oss_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormAliyunOSSFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormBaiduCloudCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.baiducloud_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.baiducloud_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormBaiduCloudCDNFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormBytePlusCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.byteplus_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.byteplus_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.byteplus_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.byteplus_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormBytePlusCDNFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormDogeCloudCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.dogecloud_cdn_domain.placeholder") })
.refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.dogecloud_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.dogecloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.dogecloud_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormDogeCloudCDNFields;

View File

@@ -0,0 +1,45 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormHuaweiCloudCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
region: z
.string({ message: t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder"))
.trim(),
domain: z
.string({ message: t("workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder") })
.refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.huaweicloud_cdn_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_cdn_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder")} />
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.huaweicloud_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormHuaweiCloudCDNFields;

View File

@@ -0,0 +1,111 @@
import { useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
const RESOURCE_TYPE_CERTIFICATE = "certificate" as const;
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const DeployNodeFormHuaweiCloudELBFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_CERTIFICATE), z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.huaweicloud_elb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder"))
.trim(),
certificateId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder")),
loadbalancerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine(
(v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(),
t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder")
),
listenerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldResourceType = Form.useWatch("resourceType", formInst);
return (
<>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.huaweicloud_elb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.huaweicloud_elb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_CERTIFICATE} value={RESOURCE_TYPE_CERTIFICATE}>
{t("workflow_node.deploy.form.huaweicloud_elb_resource_type.option.certificate.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.huaweicloud_elb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.huaweicloud_elb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.huaweicloud_elb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_elb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>
<Form.Item
name="certificateId"
label={t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerId"
label={t("workflow_node.deploy.form.huaweicloud_elb_listener_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.huaweicloud_elb_listener_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder")} />
</Form.Item>
</Show>
</>
);
};
export default DeployNodeFormHuaweiCloudELBFields;

View File

@@ -0,0 +1,77 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const DeployNodeFormKubernetesSecretFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
namespace: z
.string({ message: t("workflow_node.deploy.form.k8s_namespace.placeholder") })
.nonempty(t("workflow_node.deploy.form.k8s_namespace.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
secretName: z
.string({ message: t("workflow_node.deploy.form.k8s_secret_name.placeholder") })
.nonempty(t("workflow_node.deploy.form.k8s_secret_name.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
secretDataKeyForCrt: z
.string({ message: t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder") })
.nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
secretDataKeyForKey: z
.string({ message: t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder") })
.nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="namespace"
label={t("workflow_node.deploy.form.k8s_namespace.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.k8s_namespace.tooltip") }}></span>}
initialValue="default"
>
<Input placeholder={t("workflow_node.deploy.form.k8s_namespace.placeholder")} />
</Form.Item>
<Form.Item
name="secretName"
label={t("workflow_node.deploy.form.k8s_secret_name.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.k8s_secret_name.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.k8s_secret_name.placeholder")} />
</Form.Item>
<Form.Item
name="secretDataKeyForCrt"
label={t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.tooltip") }}></span>}
initialValue="tls.crt"
>
<Input placeholder={t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder")} />
</Form.Item>
<Form.Item
name="secretDataKeyForKey"
label={t("workflow_node.deploy.form.k8s_secret_data_key_for_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip") }}></span>}
initialValue="tls.key"
>
<Input placeholder={t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormKubernetesSecretFields;

View File

@@ -0,0 +1,305 @@
import { useTranslation } from "react-i18next";
import { Button, Dropdown, Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
import { z } from "zod";
import Show from "@/components/Show";
const FORMAT_PEM = "pem" as const;
const FORMAT_PFX = "pfx" as const;
const FORMAT_JKS = "jks" as const;
const SHELLENV_SH = "sh" as const;
const SHELLENV_CMD = "cmd" as const;
const SHELLENV_POWERSHELL = "powershell" as const;
const DeployNodeFormLocalFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
format: z.union([z.literal(FORMAT_PEM), z.literal(FORMAT_PFX), z.literal(FORMAT_JKS)], {
message: t("domain.deployment.form.local_format.placeholder"),
}),
certPath: z
.string()
.min(1, t("workflow_node.deploy.form.local_cert_path.tooltip"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
keyPath: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.local_key_path.tooltip") }),
pfxPassword: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), { message: t("workflow_node.deploy.form.local_pfx_password.tooltip") }),
jksAlias: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_alias.tooltip") }),
jksKeypass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_keypass.tooltip") }),
jksStorepass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_storepass.tooltip") }),
shellEnv: z.union([z.literal(SHELLENV_SH), z.literal(SHELLENV_CMD), z.literal(SHELLENV_POWERSHELL)], {
message: t("domain.deployment.form.shell.placeholder"),
}),
preCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
postCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldFormat = Form.useWatch("format", formInst);
const fieldCertPath = Form.useWatch("certPath", formInst);
const handleFormatSelect = (value: string) => {
if (fieldFormat === value) return;
switch (value) {
case FORMAT_PEM:
{
if (/(.pfx|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt"));
}
}
break;
case FORMAT_PFX:
{
if (/(.crt|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx"));
}
}
break;
case FORMAT_JKS:
{
if (/(.crt|.pfx)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks"));
}
}
break;
}
};
const handlePresetScriptClick = (key: string) => {
switch (key) {
case "reload_nginx":
{
formInst.setFieldValue("shellEnv", "sh");
formInst.setFieldValue("postCommand", "sudo service nginx reload");
}
break;
case "binding_iis":
{
formInst.setFieldValue("shellEnv", "powershell");
formInst.setFieldValue(
"postCommand",
`# 请将以下变量替换为实际值
$pfxPath = "<your-pfx-path>" # PFX 文件路径
$pfxPassword = "<your-pfx-password>" # PFX 密码
$siteName = "<your-site-name>" # IIS 网站名称
$domain = "<your-domain-name>" # 域名
$ipaddr = "<your-binding-ip>" # 绑定 IP“*”表示所有 IP 绑定
$port = "<your-binding-port>" # 绑定端口
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
# 导入 WebAdministration 模块
Import-Module WebAdministration
# 检查是否已存在 HTTPS 绑定
$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue
if (!$existingBinding) {
# 添加新的 HTTPS 绑定
New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
}
# 获取绑定对象
$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
# 绑定 SSL 证书
$binding.AddSslCertificate($thumbprint, "My")
# 删除目录下的证书文件
Remove-Item -Path "$pfxPath" -Force
`.trim()
);
}
break;
case "binding_netsh":
{
formInst.setFieldValue("shellEnv", "powershell");
formInst.setFieldValue(
"postCommand",
`# 请将以下变量替换为实际值
$pfxPath = "<your-pfx-path>" # PFX 文件路径
$pfxPassword = "<your-pfx-password>" # PFX 密码
$ipaddr = "<your-binding-ip>" # 绑定 IP“0.0.0.0”表示所有 IP 绑定,可填入域名。
$port = "<your-binding-port>" # 绑定端口
$addr = $ipaddr + ":" + $port
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
# 检测端口是否绑定证书,如绑定则删除绑定
$isExist = netsh http show sslcert ipport=$addr
if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr }
# 绑定到端口
netsh http add sslcert ipport=$addr certhash=$thumbprint
# 删除目录下的证书文件
Remove-Item -Path "$pfxPath" -Force
`.trim()
);
}
break;
}
};
return (
<>
<Form.Item name="format" label={t("workflow_node.deploy.form.local_format.label")} rules={[formRule]} initialValue={FORMAT_PEM}>
<Select placeholder={t("workflow_node.deploy.form.local_format.placeholder")} onSelect={handleFormatSelect}>
<Select.Option key={FORMAT_PEM} value={FORMAT_PEM}>
{t("workflow_node.deploy.form.local_format.option.pem.label")}
</Select.Option>
<Select.Option key={FORMAT_PFX} value={FORMAT_PFX}>
{t("workflow_node.deploy.form.local_format.option.pfx.label")}
</Select.Option>
<Select.Option key={FORMAT_JKS} value={FORMAT_JKS}>
{t("workflow_node.deploy.form.local_format.option.jks.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="certPath"
label={t("workflow_node.deploy.form.local_cert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_cert_path.tooltip") }}></span>}
initialValue="/etc/ssl/certs/cert.crt"
>
<Input placeholder={t("workflow_node.deploy.form.local_cert_path.placeholder")} />
</Form.Item>
<Show when={fieldFormat === FORMAT_PEM}>
<Form.Item
name="keyPath"
label={t("workflow_node.deploy.form.local_key_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_key_path.tooltip") }}></span>}
initialValue="/etc/ssl/certs/cert.key"
>
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
<Form.Item name="pfxPassword" label={t("workflow_node.deploy.form.local_pfx_password.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.local_pfx_password.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_JKS}>
<Form.Item name="jksAlias" label={t("workflow_node.deploy.form.local_jks_alias.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.local_jks_alias.placeholder")} />
</Form.Item>
<Form.Item name="jksKeypass" label={t("workflow_node.deploy.form.local_jks_keypass.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.local_jks_keypass.placeholder")} />
</Form.Item>
<Form.Item name="jksStorepass" label={t("workflow_node.deploy.form.local_jks_storepass.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.local_jks_storepass.placeholder")} />
</Form.Item>
</Show>
<Form.Item name="shellEnv" label={t("workflow_node.deploy.form.local_shell_env.label")} rules={[formRule]} initialValue={SHELLENV_SH}>
<Select placeholder={t("workflow_node.deploy.form.local_shell_env.placeholder")}>
<Select.Option key={SHELLENV_SH} value={SHELLENV_SH}>
{t("workflow_node.deploy.form.local_shell_env.option.sh.label")}
</Select.Option>
<Select.Option key={SHELLENV_CMD} value={SHELLENV_CMD}>
{t("workflow_node.deploy.form.local_shell_env.option.cmd.label")}
</Select.Option>
<Select.Option key={SHELLENV_POWERSHELL} value={SHELLENV_POWERSHELL}>
{t("workflow_node.deploy.form.local_shell_env.option.powershell.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item name="preCommand" label={t("workflow_node.deploy.form.local_pre_command.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")} />
</Form.Item>
<Form.Item className="mb-0">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">
<span>{t("workflow_node.deploy.form.local_post_command.label")}</span>
</div>
<div className="text-right">
<Dropdown
menu={{
items: [
{
key: "reload_nginx",
label: t("workflow_node.deploy.form.local_preset_scripts.option.reload_nginx.label"),
onClick: () => handlePresetScriptClick("reload_nginx"),
},
{
key: "binding_iis",
label: t("workflow_node.deploy.form.local_preset_scripts.option.binding_iis.label"),
onClick: () => handlePresetScriptClick("binding_iis"),
},
{
key: "binding_netsh",
label: t("workflow_node.deploy.form.local_preset_scripts.option.binding_netsh.label"),
onClick: () => handlePresetScriptClick("binding_netsh"),
},
],
}}
trigger={["click"]}
>
<Button size="small" type="link">
{t("workflow_node.deploy.form.local_preset_scripts.button")}
<DownOutlinedIcon />
</Button>
</Dropdown>
</div>
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")} />
</Form.Item>
</Form.Item>
</>
);
};
export default DeployNodeFormLocalFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormQiniuCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.qiniu_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.qiniu_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.qiniu_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormQiniuCDNFields;

View File

@@ -0,0 +1,211 @@
import { useTranslation } from "react-i18next";
import { Button, Dropdown, Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
import { z } from "zod";
import Show from "@/components/Show";
const FORMAT_PEM = "pem" as const;
const FORMAT_PFX = "pfx" as const;
const FORMAT_JKS = "jks" as const;
const DeployNodeFormSSHFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
format: z.union([z.literal(FORMAT_PEM), z.literal(FORMAT_PFX), z.literal(FORMAT_JKS)], {
message: t("domain.deployment.form.ssh_format.placeholder"),
}),
certPath: z
.string()
.min(1, t("workflow_node.deploy.form.ssh_cert_path.tooltip"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
keyPath: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_key_path.tooltip") }),
pfxPassword: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_pfx_password.tooltip") }),
jksAlias: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_jks_alias.tooltip") }),
jksKeypass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_jks_keypass.tooltip") }),
jksStorepass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_jks_storepass.tooltip") }),
preCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
postCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldFormat = Form.useWatch("format", formInst);
const fieldCertPath = Form.useWatch("certPath", formInst);
const handleFormatSelect = (value: string) => {
if (fieldFormat === value) return;
switch (value) {
case FORMAT_PEM:
{
if (/(.pfx|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt"));
}
}
break;
case FORMAT_PFX:
{
if (/(.crt|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx"));
}
}
break;
case FORMAT_JKS:
{
if (/(.crt|.pfx)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks"));
}
}
break;
}
};
const handlePresetScriptClick = (key: string) => {
switch (key) {
case "reload_nginx":
{
formInst.setFieldValue("postCommand", "sudo service nginx reload");
}
break;
}
};
return (
<>
<Form.Item name="format" label={t("workflow_node.deploy.form.ssh_format.label")} rules={[formRule]} initialValue={FORMAT_PEM}>
<Select placeholder={t("workflow_node.deploy.form.ssh_format.placeholder")} onSelect={handleFormatSelect}>
<Select.Option key={FORMAT_PEM} value={FORMAT_PEM}>
{t("workflow_node.deploy.form.ssh_format.option.pem.label")}
</Select.Option>
<Select.Option key={FORMAT_PFX} value={FORMAT_PFX}>
{t("workflow_node.deploy.form.ssh_format.option.pfx.label")}
</Select.Option>
<Select.Option key={FORMAT_JKS} value={FORMAT_JKS}>
{t("workflow_node.deploy.form.ssh_format.option.jks.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="certPath"
label={t("workflow_node.deploy.form.ssh_cert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_cert_path.tooltip") }}></span>}
initialValue="/etc/ssl/certs/cert.crt"
>
<Input placeholder={t("workflow_node.deploy.form.ssh_cert_path.placeholder")} />
</Form.Item>
<Show when={fieldFormat === FORMAT_PEM}>
<Form.Item
name="keyPath"
label={t("workflow_node.deploy.form.ssh_key_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_key_path.tooltip") }}></span>}
initialValue="/etc/ssl/certs/cert.key"
>
<Input placeholder={t("workflow_node.deploy.form.ssh_key_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
<Form.Item name="pfxPassword" label={t("workflow_node.deploy.form.ssh_pfx_password.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.ssh_pfx_password.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_JKS}>
<Form.Item name="jksAlias" label={t("workflow_node.deploy.form.ssh_jks_alias.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.ssh_jks_alias.placeholder")} />
</Form.Item>
<Form.Item name="jksKeypass" label={t("workflow_node.deploy.form.ssh_jks_keypass.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.ssh_jks_keypass.placeholder")} />
</Form.Item>
<Form.Item name="jksStorepass" label={t("workflow_node.deploy.form.ssh_jks_storepass.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.ssh_jks_storepass.placeholder")} />
</Form.Item>
</Show>
<Form.Item label={t("workflow_node.deploy.form.ssh_shell_env.label")}>
<Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} />
</Form.Item>
<Form.Item name="preCommand" label={t("workflow_node.deploy.form.ssh_pre_command.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")} />
</Form.Item>
<Form.Item className="mb-0">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">
<span>{t("workflow_node.deploy.form.ssh_post_command.label")}</span>
</div>
<div className="text-right">
<Dropdown
menu={{
items: [
{
key: "reload_nginx",
label: t("workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label"),
onClick: () => handlePresetScriptClick("reload_nginx"),
},
],
}}
trigger={["click"]}
>
<Button size="small" type="link">
{t("workflow_node.deploy.form.ssh_preset_scripts.button")}
<DownOutlinedIcon />
</Button>
</Dropdown>
</div>
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} />
</Form.Item>
</Form.Item>
</>
);
};
export default DeployNodeFormSSHFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormTencentCloudCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormTencentCloudCDNFields;

View File

@@ -0,0 +1,125 @@
import { useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { validDomainName } from "@/utils/validators";
const RESOURCE_TYPE_SSLDEPLOY = "ssl-deploy" as const;
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const RESOURCE_TYPE_RULEDOMAIN = "ruledomain" as const;
const DeployNodeFormTencentCloudCLBFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union(
[z.literal(RESOURCE_TYPE_SSLDEPLOY), z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER), z.literal(RESOURCE_TYPE_RULEDOMAIN)],
{ message: t("workflow_node.deploy.form.tencentcloud_clb_resource_type.placeholder") }
),
region: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder"))
.trim(),
loadbalancerId: z
.string()
.min(1, t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
listenerId: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish()
.refine(
(v) => ![RESOURCE_TYPE_SSLDEPLOY, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN].includes(fieldResourceType) || !!v?.trim(),
t("workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder")
),
domain: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_clb_domain.placeholder") })
.nullish()
.refine((v) => RESOURCE_TYPE_RULEDOMAIN !== fieldResourceType || validDomainName(v!, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const fieldResourceType = Form.useWatch("resourceType", formInst);
return (
<>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.tencentcloud_clb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.tencentcloud_clb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_SSLDEPLOY} value={RESOURCE_TYPE_SSLDEPLOY}>
{t("workflow_node.deploy.form.tencentcloud_clb_resource_type.option.ssl_deploy.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.tencentcloud_clb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.tencentcloud_clb_resource_type.option.listener.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_RULEDOMAIN} value={RESOURCE_TYPE_RULEDOMAIN}>
{t("workflow_node.deploy.form.tencentcloud_clb_resource_type.option.ruledomain.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.tencentcloud_clb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder")} />
</Form.Item>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder")} />
</Form.Item>
<Show
when={fieldResourceType === RESOURCE_TYPE_SSLDEPLOY || fieldResourceType === RESOURCE_TYPE_LISTENER || fieldResourceType === RESOURCE_TYPE_RULEDOMAIN}
>
<Form.Item
name="listenerId"
label={t("workflow_node.deploy.form.tencentcloud_clb_listener_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_listener_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_SSLDEPLOY}>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_clb_snidomain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_snidomain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_snidomain.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_RULEDOMAIN}>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_clb_ruledomain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_ruledomain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_ruledomain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
export default DeployNodeFormTencentCloudCLBFields;

View File

@@ -0,0 +1,58 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormTencentCloudCOSFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
region: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder"))
.trim(),
bucket: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder") })
.nonempty(t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder"))
.trim(),
domain: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_cos_domain.placeholder") })
.refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.tencentcloud_cos_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cos_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder")} />
</Form.Item>
<Form.Item
name="bucket"
label={t("workflow_node.deploy.form.tencentcloud_cos_bucket.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cos_bucket.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder")} />
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_cos_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cos_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cos_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormTencentCloudCOSFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormTencentCloudECDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ecdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormTencentCloudECDNFields;

View File

@@ -0,0 +1,45 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormTencentCloudEOFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
zoneId: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder") })
.nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder"))
.trim(),
domain: z
.string({ message: t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder") })
.refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="zoneId"
label={t("workflow_node.deploy.form.tencentcloud_eo_zone_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")} />
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_eo_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_eo_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormTencentCloudEOFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormVolcEngineCDNFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.volcengine_cdn_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.volcengine_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.volcengine_cdn_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormVolcEngineCDNFields;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
const DeployNodeFormVolcEngineLiveFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
domain: z
.string({ message: t("workflow_node.deploy.form.volcengine_live_domain.placeholder") })
.refine((v) => validDomainName(v, true), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.volcengine_live_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_live_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.volcengine_live_domain.placeholder")} />
</Form.Item>
</>
);
};
export default DeployNodeFormVolcEngineLiveFields;

View File

@@ -0,0 +1,50 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const DeployNodeFormWebhookFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
webhookData: z.string({ message: t("workflow_node.deploy.form.webhook_data.placeholder") }).refine((v) => {
try {
JSON.parse(v);
return true;
} catch {
return false;
}
}, t("workflow_node.deploy.form.webhook_data.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("webhookData", json);
} catch {
return;
}
};
return (
<>
<Form.Item
name="webhookData"
label={t("workflow_node.deploy.form.webhook_data.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.tooltip") }}></span>}
>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>
</Form.Item>
</>
);
};
export default DeployNodeFormWebhookFields;

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { Button, Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
import { produce } from "immer";
import { z } from "zod";
import { usePanel } from "../PanelProvider";
@@ -37,14 +38,14 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
const formSchema = z.object({
subject: z
.string({ message: t("workflow.nodes.notify.form.subject.placeholder") })
.min(1, t("workflow.nodes.notify.form.subject.placeholder"))
.string({ message: t("workflow_node.notify.form.subject.placeholder") })
.min(1, t("workflow_node.notify.form.subject.placeholder"))
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
message: z
.string({ message: t("workflow.nodes.notify.form.message.placeholder") })
.min(1, t("workflow.nodes.notify.form.message.placeholder"))
.string({ message: t("workflow_node.notify.form.message.placeholder") })
.min(1, t("workflow_node.notify.form.message.placeholder"))
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
channel: z.string({ message: t("workflow.nodes.notify.form.channel.placeholder") }).min(1, t("workflow.nodes.notify.form.channel.placeholder")),
channel: z.string({ message: t("workflow_node.notify.form.channel.placeholder") }).min(1, t("workflow_node.notify.form.channel.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const {
@@ -54,29 +55,35 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
} = useAntdForm<z.infer<typeof formSchema>>({
initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => {
await updateNode({ ...data, config: { ...values }, validated: true });
await formInst.validateFields();
await updateNode(
produce(data, (draft) => {
draft.config = { ...values };
draft.validated = true;
})
);
hidePanel();
},
});
return (
<Form {...formProps} form={formInst} disabled={formPending} layout="vertical">
<Form.Item name="subject" label={t("workflow.nodes.notify.form.subject.label")} rules={[formRule]}>
<Input placeholder={t("workflow.nodes.notify.form.subject.placeholder")} />
<Form.Item name="subject" label={t("workflow_node.notify.form.subject.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.notify.form.subject.placeholder")} />
</Form.Item>
<Form.Item name="message" label={t("workflow.nodes.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow.nodes.notify.form.message.placeholder")} />
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item>
<Form.Item>
<Form.Item className="mb-0">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">{t("workflow.nodes.notify.form.channel.label")}</div>
<div className="flex-grow max-w-full truncate">{t("workflow_node.notify.form.channel.label")}</div>
<div className="text-right">
<Link className="ant-typography" to="/settings/notification" target="_blank">
<Button size="small" type="link">
{t("workflow.nodes.notify.form.channel.button")}
{t("workflow_node.notify.form.channel.button")}
<RightOutlinedIcon className="text-xs" />
</Button>
</Link>
@@ -92,7 +99,7 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
label: t(notifyChannelsMap.get(k)?.name ?? k),
value: k,
}))}
placeholder={t("workflow.nodes.notify.form.channel.placeholder")}
placeholder={t("workflow_node.notify.form.channel.placeholder")}
/>
</Form.Item>
</Form.Item>

View File

@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { Alert, Button, Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import dayjs from "dayjs";
import { produce } from "immer";
import { z } from "zod";
import { usePanel } from "../PanelProvider";
@@ -30,7 +31,7 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => {
const formSchema = z
.object({
executionMethod: z.string({ message: t("workflow.nodes.start.form.trigger.placeholder") }).min(1, t("workflow.nodes.start.form.trigger.placeholder")),
executionMethod: z.string({ message: t("workflow_node.start.form.trigger.placeholder") }).min(1, t("workflow_node.start.form.trigger.placeholder")),
crontab: z.string().nullish(),
})
.superRefine((data, ctx) => {
@@ -41,7 +42,7 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => {
if (!validCronExpression(data.crontab!)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("workflow.nodes.start.form.trigger_cron.errmsg.invalid"),
message: t("workflow_node.start.form.trigger_cron.errmsg.invalid"),
path: ["crontab"],
});
}
@@ -54,7 +55,13 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => {
} = useAntdForm<z.infer<typeof formSchema>>({
initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => {
await updateNode({ ...data, config: { ...values }, validated: true });
await formInst.validateFields();
await updateNode(
produce(data, (draft) => {
draft.config = { ...values };
draft.validated = true;
})
);
hidePanel();
},
});
@@ -82,26 +89,26 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => {
<Form {...formProps} form={formInst} disabled={formPending} layout="vertical">
<Form.Item
name="executionMethod"
label={t("workflow.nodes.start.form.trigger.label")}
label={t("workflow_node.start.form.trigger.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.start.form.trigger.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger.tooltip") }}></span>}
>
<Radio.Group value={triggerType} onChange={(e) => handleTriggerTypeChange(e.target.value)}>
<Radio value="auto">{t("workflow.nodes.start.form.trigger.option.auto.label")}</Radio>
<Radio value="manual">{t("workflow.nodes.start.form.trigger.option.manual.label")}</Radio>
<Radio value="auto">{t("workflow_node.start.form.trigger.option.auto.label")}</Radio>
<Radio value="manual">{t("workflow_node.start.form.trigger.option.manual.label")}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="crontab"
label={t("workflow.nodes.start.form.trigger_cron.label")}
label={t("workflow_node.start.form.trigger_cron.label")}
hidden={triggerType !== "auto"}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.start.form.trigger_cron.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron.tooltip") }}></span>}
extra={
triggerCronLastExecutions.length > 0 ? (
<div>
{t("workflow.nodes.start.form.trigger_cron.extra")}
{t("workflow_node.start.form.trigger_cron.extra")}
<br />
{triggerCronLastExecutions.map((date, index) => (
<span key={index}>
@@ -115,11 +122,11 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => {
)
}
>
<Input placeholder={t("workflow.nodes.start.form.trigger_cron.placeholder")} onChange={(e) => handleTriggerCronChange(e.target.value)} />
<Input placeholder={t("workflow_node.start.form.trigger_cron.placeholder")} onChange={(e) => handleTriggerCronChange(e.target.value)} />
</Form.Item>
<Form.Item hidden={triggerType !== "auto"}>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.start.form.trigger_cron_alert.content") }}></span>} />
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron_alert.content") }}></span>} />
</Form.Item>
<Form.Item>