refactor: workflow monitor(aka inspect) node

This commit is contained in:
Fu Diwei
2025-05-28 21:05:56 +08:00
parent 4489096e57
commit 3a829ad53b
35 changed files with 557 additions and 516 deletions

View File

@@ -120,7 +120,7 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
<div className="w-1/3">
<Form.Item name="port" label={t("access.form.ssh_port.label")} rules={[formRule]}>
<InputNumber className="w-full" placeholder={t("access.form.ssh_port.placeholder")} min={1} max={65535} />
<InputNumber className="w-full" min={1} max={65535} placeholder={t("access.form.ssh_port.placeholder")} />
</Form.Item>
</div>
</div>

View File

@@ -37,7 +37,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
if (!access) {
return (
<Space className="max-w-full grow truncate" size={4}>
<Avatar size="small" />
<Avatar shape="square" size="small" />
<Typography.Text className="leading-loose" ellipsis>
{key}
</Typography.Text>
@@ -48,7 +48,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
const provider = accessProvidersMap.get(access.provider);
return (
<Space className="max-w-full grow truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{access.name}
</Typography.Text>

View File

@@ -67,7 +67,7 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, filter, placehol
}}
>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<Avatar shape="square" src={provider.icon} size="small" />
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
</Flex>
</Card>

View File

@@ -32,7 +32,7 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr
const provider = acmeDns01ProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>

View File

@@ -86,12 +86,12 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
}}
>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<Avatar shape="square" src={provider.icon} size="small" />
<div className="flex-1 overflow-hidden">
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
{t(provider.name)}
</Typography.Text>
<div className="origin-left scale-[80%]">
<div className="origin-left scale-[75%]">
<Show when={provider.builtin}>
<Tag>{t("access.props.provider.builtin")}</Tag>
</Show>

View File

@@ -49,12 +49,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
return (
<div className="flex max-w-full items-center justify-between gap-4 overflow-hidden">
<Space className="max-w-full grow truncate" size={4}>
<Avatar src={provider.icon} size="small" />
<Avatar shape="square" src={provider.icon} size="small" />
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} ellipsis>
{t(provider.name)}
</Typography.Text>
</Space>
<div className="origin-right scale-[80%]">
<div className="origin-right scale-[75%]">
<Show when={provider.builtin}>
<Tag>{t("access.props.provider.builtin")}</Tag>
</Show>

View File

@@ -48,7 +48,7 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
const provider = caProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>

View File

@@ -104,7 +104,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
>
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<Avatar shape="square" src={provider.icon} size="small" />
<Typography.Text className="line-clamp-2 flex-1">{t(provider.name)}</Typography.Text>
</Flex>
</Tooltip>

View File

@@ -32,7 +32,7 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect
const provider = deploymentProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>

View File

@@ -32,7 +32,7 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe
const provider = notificationProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>

View File

@@ -9,10 +9,11 @@ import DeployNode from "./node/DeployNode";
import EndNode from "./node/EndNode";
import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode";
import ExecuteResultNode from "./node/ExecuteResultNode";
import MonitorNode from "./node/MonitorNode";
import NotifyNode from "./node/NotifyNode";
import StartNode from "./node/StartNode";
import UnknownNode from "./node/UnknownNode";
import UploadNode from "./node/UploadNode";
import InspectNode from "./node/InspectNode";
export type WorkflowElementProps = {
node: WorkflowNode;
@@ -32,9 +33,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
case WorkflowNodeType.Upload:
return <UploadNode node={node} disabled={disabled} />;
case WorkflowNodeType.Inspect:
return <InspectNode node={node} disabled={disabled} />;
case WorkflowNodeType.Monitor:
return <MonitorNode node={node} disabled={disabled} />;
case WorkflowNodeType.Deploy:
return <DeployNode node={node} disabled={disabled} />;
@@ -60,7 +61,7 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
default:
console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
return <></>;
return <UnknownNode node={node} />;
}
}, [node, disabled, branchId, branchIndex]);

View File

@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
import {
CloudUploadOutlined as CloudUploadOutlinedIcon,
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
MonitorOutlined as MonitorOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
SendOutlined as SendOutlinedIcon,
SisternodeOutlined as SisternodeOutlinedIcon,
SolutionOutlined as SolutionOutlinedIcon,
MonitorOutlined as MonitorOutlinedIcon,
} from "@ant-design/icons";
import { Dropdown } from "antd";
@@ -28,7 +28,7 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
return [
[WorkflowNodeType.Apply, "workflow_node.apply.label", <SolutionOutlinedIcon />],
[WorkflowNodeType.Upload, "workflow_node.upload.label", <CloudUploadOutlinedIcon />],
[WorkflowNodeType.Inspect, "workflow_node.inspect.label", <MonitorOutlinedIcon />],
[WorkflowNodeType.Monitor, "workflow_node.monitor.label", <MonitorOutlinedIcon />],
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", <DeploymentUnitOutlinedIcon />],
[WorkflowNodeType.Notify, "workflow_node.notify.label", <SendOutlinedIcon />],
[WorkflowNodeType.Branch, "workflow_node.branch.label", <SisternodeOutlinedIcon />],

View File

@@ -1,14 +1,17 @@
import { memo, useRef, useState } from "react";
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
import { Button, Card, Popover } from "antd";
import { produce } from "immer";
import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow";
import { ExprType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
import AddNode from "./AddNode";
import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow";
import { produce } from "immer";
import { useWorkflowStore } from "@/stores/workflow";
import { useZustandShallowSelector } from "@/hooks";
import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
import ConditionNodeConfigForm from "./ConditionNodeConfigForm";
export type ConditionNodeProps = SharedNodeProps & {
branchId: string;

View File

@@ -46,7 +46,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
const provider = deploymentProvidersMap.get(config.provider);
return (
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="flex-1 truncate">{t(provider?.name ?? "")}</Typography.Text>
</Flex>
);

View File

@@ -1,97 +0,0 @@
import { forwardRef, memo, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type WorkflowNodeConfigForInspect } from "@/domain/workflow";
import { useAntdForm } from "@/hooks";
import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators";
type InspectNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForInspect>;
export type InspectNodeConfigFormProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: InspectNodeConfigFormFieldValues;
onValuesChange?: (values: InspectNodeConfigFormFieldValues) => void;
};
export type InspectNodeConfigFormInstance = {
getFieldsValue: () => ReturnType<FormInstance<InspectNodeConfigFormFieldValues>["getFieldsValue"]>;
resetFields: FormInstance<InspectNodeConfigFormFieldValues>["resetFields"];
validateFields: FormInstance<InspectNodeConfigFormFieldValues>["validateFields"];
};
const initFormModel = (): InspectNodeConfigFormFieldValues => {
return {
domain: "",
port: "443",
path: "",
host: "",
};
};
const InspectNodeConfigForm = forwardRef<InspectNodeConfigFormInstance, InspectNodeConfigFormProps>(
({ className, style, disabled, initialValues, onValuesChange }, ref) => {
const { t } = useTranslation();
const formSchema = z.object({
host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), {
message: t("workflow_node.inspect.form.host.placeholder"),
}),
domain: z.string().optional(),
port: z.string().refine((val) => validPortNumber(val), {
message: t("workflow_node.inspect.form.port.placeholder"),
}),
path: z.string().optional(),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({
name: "workflowNodeInspectConfigForm",
initialValues: initialValues ?? initFormModel(),
});
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values as InspectNodeConfigFormFieldValues);
};
useImperativeHandle(ref, () => {
return {
getFieldsValue: () => {
return formInst.getFieldsValue(true);
},
resetFields: (fields) => {
return formInst.resetFields(fields as (keyof InspectNodeConfigFormFieldValues)[]);
},
validateFields: (nameList, config) => {
return formInst.validateFields(nameList, config);
},
} as InspectNodeConfigFormInstance;
});
return (
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item name="host" label={t("workflow_node.inspect.form.host.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.inspect.form.host.placeholder")} />
</Form.Item>
<Form.Item name="port" label={t("workflow_node.inspect.form.port.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.inspect.form.port.placeholder")} />
</Form.Item>
<Form.Item name="domain" label={t("workflow_node.inspect.form.domain.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.inspect.form.domain.placeholder")} />
</Form.Item>
<Form.Item name="path" label={t("workflow_node.inspect.form.path.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.inspect.form.path.placeholder")} />
</Form.Item>
</Form>
);
}
);
export default memo(InspectNodeConfigForm);

View File

@@ -3,43 +3,43 @@ import { useTranslation } from "react-i18next";
import { Flex, Typography } from "antd";
import { produce } from "immer";
import { type WorkflowNodeConfigForInspect, WorkflowNodeType } from "@/domain/workflow";
import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
import InspectNodeConfigForm, { type InspectNodeConfigFormInstance } from "./InspectNodeConfigForm";
import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm";
export type InspectNodeProps = SharedNodeProps;
export type MonitorNodeProps = SharedNodeProps;
const InspectNode = ({ node, disabled }: InspectNodeProps) => {
if (node.type !== WorkflowNodeType.Inspect) {
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`);
const MonitorNode = ({ node, disabled }: MonitorNodeProps) => {
if (node.type !== WorkflowNodeType.Monitor) {
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
}
const { t } = useTranslation();
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
const formRef = useRef<InspectNodeConfigFormInstance>(null);
const formRef = useRef<MonitorNodeConfigFormInstance>(null);
const [formPending, setFormPending] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForInspect;
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor;
const wrappedEl = useMemo(() => {
if (node.type !== WorkflowNodeType.Inspect) {
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`);
if (node.type !== WorkflowNodeType.Monitor) {
console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
}
if (!node.validated) {
return <Typography.Link>{t("workflow_node.action.configure_node")}</Typography.Link>;
}
const config = (node.config as WorkflowNodeConfigForInspect) ?? {};
const config = (node.config as WorkflowNodeConfigForMonitor) ?? {};
return (
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Typography.Text className="truncate">{config.host ?? ""}</Typography.Text>
<Typography.Text className="truncate">{config.domain || config.host || ""}</Typography.Text>
</Flex>
);
}, [node]);
@@ -81,10 +81,10 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => {
onOpenChange={(open) => setDrawerOpen(open)}
getFormValues={() => formRef.current!.getFieldsValue()}
>
<InspectNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
<MonitorNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
</SharedNode.ConfigDrawer>
</>
);
};
export default memo(InspectNode);
export default memo(MonitorNode);

View File

@@ -0,0 +1,115 @@
import { forwardRef, memo, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { Alert, Form, type FormInstance, Input, InputNumber } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow";
import { useAntdForm } from "@/hooks";
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
type MonitorNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForMonitor>;
export type MonitorNodeConfigFormProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: MonitorNodeConfigFormFieldValues;
onValuesChange?: (values: MonitorNodeConfigFormFieldValues) => void;
};
export type MonitorNodeConfigFormInstance = {
getFieldsValue: () => ReturnType<FormInstance<MonitorNodeConfigFormFieldValues>["getFieldsValue"]>;
resetFields: FormInstance<MonitorNodeConfigFormFieldValues>["resetFields"];
validateFields: FormInstance<MonitorNodeConfigFormFieldValues>["validateFields"];
};
const initFormModel = (): MonitorNodeConfigFormFieldValues => {
return {
host: "",
port: 443,
requestPath: "/",
};
};
const MonitorNodeConfigForm = forwardRef<MonitorNodeConfigFormInstance, MonitorNodeConfigFormProps>(
({ className, style, disabled, initialValues, onValuesChange }, ref) => {
const { t } = useTranslation();
const formSchema = z.object({
host: z.string().refine((v) => {
return validDomainName(v) || validIPv4Address(v) || validIPv6Address(v);
}, t("common.errmsg.host_invalid")),
port: z.preprocess(
(v) => Number(v),
z
.number()
.int(t("workflow_node.monitor.form.port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
domain: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return validDomainName(v);
}, t("common.errmsg.domain_invalid")),
requestPath: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({
name: "workflowNodeMonitorConfigForm",
initialValues: initialValues ?? initFormModel(),
});
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values as MonitorNodeConfigFormFieldValues);
};
useImperativeHandle(ref, () => {
return {
getFieldsValue: () => {
return formInst.getFieldsValue(true);
},
resetFields: (fields) => {
return formInst.resetFields(fields as (keyof MonitorNodeConfigFormFieldValues)[]);
},
validateFields: (nameList, config) => {
return formInst.validateFields(nameList, config);
},
} as MonitorNodeConfigFormInstance;
});
return (
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.monitor.form.guide") }}></span>} />
</Form.Item>
<div className="flex space-x-2">
<div className="w-2/3">
<Form.Item name="host" label={t("workflow_node.monitor.form.host.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.monitor.form.host.placeholder")} />
</Form.Item>
</div>
<div className="w-1/3">
<Form.Item name="port" label={t("workflow_node.monitor.form.port.label")} rules={[formRule]}>
<InputNumber className="w-full" min={1} max={65535} placeholder={t("workflow_node.monitor.form.port.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item name="domain" label={t("workflow_node.monitor.form.domain.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.monitor.form.domain.placeholder")} />
</Form.Item>
<Form.Item name="requestPath" label={t("workflow_node.monitor.form.request_path.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.monitor.form.request_path.placeholder")} />
</Form.Item>
</Form>
);
}
);
export default memo(MonitorNodeConfigForm);

View File

@@ -43,7 +43,7 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
const provider = notificationProvidersMap.get(config.provider);
return (
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider?.icon} size="small" />
<Avatar shape="square" src={provider?.icon} size="small" />
<Typography.Text className="flex-1 truncate">{t(channel?.name ?? provider?.name ?? " ")}</Typography.Text>
<Typography.Text className="truncate" type="secondary">
{config.subject ?? ""}

View File

@@ -0,0 +1,45 @@
import { memo } from "react";
import { CloseCircleOutlined as CloseCircleOutlinedIcon } from "@ant-design/icons";
import { Alert, Button, Card } from "antd";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import { type SharedNodeProps } from "./_SharedNode";
import AddNode from "./AddNode";
export type MonitorNodeProps = SharedNodeProps;
const UnknownNode = ({ node, disabled }: MonitorNodeProps) => {
const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"]));
const handleClickRemove = () => {
removeNode(node.id);
};
return (
<>
<Card className="relative w-[256px] overflow-hidden shadow-md" styles={{ body: { padding: 0 } }} hoverable variant="borderless">
<div className="cursor-pointer ">
<Alert
type="error"
message={
<div className="flex items-center justify-between gap-4 overflow-hidden">
<div className="flex-1 text-center text-xs">
INVALID NODE
<br />
PLEASE REMOVE
</div>
<Button color="primary" icon={<CloseCircleOutlinedIcon />} variant="text" onClick={handleClickRemove} />
</div>
}
/>
</div>
</Card>
<AddNode node={node} disabled={disabled} />
</>
);
};
export default memo(UnknownNode);

View File

@@ -553,6 +553,14 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.UCLOUD_UCDN, "provider.ucloud.ucdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.RAINYUN_RCDN, "provider.rainyun.rcdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST, "provider.unicloud.webhost", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT, "provider.aws.cloudfront", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.AWS_ACM, "provider.aws.acm", DEPLOYMENT_CATEGORIES.SSL],
[DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT, "provider.azure.keyvault", DEPLOYMENT_CATEGORIES.SSL],
[DEPLOYMENT_PROVIDERS.BUNNY_CDN, "provider.bunny.cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.CACHEFLY, "provider.cachefly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS, "provider.edgio.applications", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.GCORE_CDN, "provider.gcore.cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.NETLIFY_SITE, "provider.netlify.site", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.CDNFLY, "provider.cdnfly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.FLEXCDN, "provider.flexcdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.GOEDGE, "provider.goedge", DEPLOYMENT_CATEGORIES.CDN],
@@ -566,14 +574,6 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.BAOTAWAF_SITE, "provider.baotawaf.site", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, "provider.baotawaf.console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT, "provider.aws.cloudfront", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.AWS_ACM, "provider.aws.acm", DEPLOYMENT_CATEGORIES.SSL],
[DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT, "provider.azure.keyvault", DEPLOYMENT_CATEGORIES.SSL],
[DEPLOYMENT_PROVIDERS.BUNNY_CDN, "provider.bunny.cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.CACHEFLY, "provider.cachefly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS, "provider.edgio.applications", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.GCORE_CDN, "provider.gcore.cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.NETLIFY_SITE, "provider.netlify.site", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS],
].map(([type, name, category, builtin]) => [
type,

View File

@@ -31,7 +31,7 @@ export enum WorkflowNodeType {
End = "end",
Apply = "apply",
Upload = "upload",
Inspect = "inspect",
Monitor = "monitor",
Deploy = "deploy",
Notify = "notify",
Branch = "branch",
@@ -43,23 +43,25 @@ export enum WorkflowNodeType {
}
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
[WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")],
[WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")],
[WorkflowNodeType.Inspect, i18n.t("workflow_node.inspect.label")],
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")],
[WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")],
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")],
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")],
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")],
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")],
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
[WorkflowNodeType.Start, i18n.t("workflow_node.start.default_name")],
[WorkflowNodeType.End, i18n.t("workflow_node.end.default_name")],
[WorkflowNodeType.Apply, i18n.t("workflow_node.apply.default_name")],
[WorkflowNodeType.Upload, i18n.t("workflow_node.upload.default_name")],
[WorkflowNodeType.Monitor, i18n.t("workflow_node.monitor.default_name")],
[WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.default_name")],
[WorkflowNodeType.Notify, i18n.t("workflow_node.notify.default_name")],
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.default_name")],
[WorkflowNodeType.Condition, i18n.t("workflow_node.condition.default_name")],
[WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")],
[WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")],
[WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")],
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.default_name")],
]);
const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
[WorkflowNodeType.Apply, []],
[WorkflowNodeType.Upload, []],
[WorkflowNodeType.Monitor, []],
[
WorkflowNodeType.Deploy,
[
@@ -98,7 +100,7 @@ const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> =
],
],
[
WorkflowNodeType.Inspect,
WorkflowNodeType.Monitor,
[
{
name: "certificate",
@@ -158,11 +160,11 @@ export type WorkflowNodeConfigForUpload = {
privateKey: string;
};
export type WorkflowNodeConfigForInspect = {
domain: string;
port: string;
export type WorkflowNodeConfigForMonitor = {
host: string;
path: string;
port: number;
domain?: string;
requestPath?: string;
};
export type WorkflowNodeConfigForDeploy = {
@@ -351,7 +353,7 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}
case WorkflowNodeType.Apply:
case WorkflowNodeType.Upload:
case WorkflowNodeType.Deploy:
case WorkflowNodeType.Inspect:
case WorkflowNodeType.Monitor:
{
node.inputs = workflowNodeTypeDefaultInputs.get(nodeType);
node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType);

View File

@@ -10,6 +10,7 @@
"workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
"workflow_node.start.label": "Start",
"workflow_node.start.default_name": "Start",
"workflow_node.start.form.trigger.label": "Trigger",
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
"workflow_node.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.<br>Manual: Manually triggered.",
@@ -22,7 +23,8 @@
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times. Don't always set it to midnight every day to avoid spikes in traffic.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",
"workflow_node.apply.label": "Application",
"workflow_node.apply.label": "Obtain certificate",
"workflow_node.apply.default_name": "Application",
"workflow_node.apply.form.domains.label": "Domains",
"workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
"workflow_node.apply.form.domains.tooltip": "Wildcard domain: *.example.com",
@@ -97,7 +99,17 @@
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
"workflow_node.deploy.label": "Deployment",
"workflow_node.upload.label": "Upload certificate",
"workflow_node.upload.default_name": "Uploading",
"workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.deploy.label": "Deploy certificate",
"workflow_node.deploy.default_name": "Deployment",
"workflow_node.deploy.form.provider.label": "Deploy target",
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
@@ -805,25 +817,20 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
"workflow_node.upload.label": "Upload",
"workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.monitor.label": "Monitor certificate",
"workflow_node.monitor.default_name": "Monitoring",
"workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.",
"workflow_node.monitor.form.host.label": "Host",
"workflow_node.monitor.form.host.placeholder": "Please enter host",
"workflow_node.monitor.form.port.label": "Port",
"workflow_node.monitor.form.port.placeholder": "Please enter port",
"workflow_node.monitor.form.domain.label": "Domain (Optional)",
"workflow_node.monitor.form.domain.placeholder": "Please enter domain name",
"workflow_node.monitor.form.request_path.label": "Request path (Optional)",
"workflow_node.monitor.form.request_path.placeholder": "Please enter request path",
"workflow_node.inspect.label": "Inspect certificate",
"workflow_node.inspect.form.domain.label": "Domain",
"workflow_node.inspect.form.domain.placeholder": "Please enter domain name",
"workflow_node.inspect.form.port.label": "Port",
"workflow_node.inspect.form.port.placeholder": "Please enter port",
"workflow_node.inspect.form.host.label": "Host",
"workflow_node.inspect.form.host.placeholder": "Please enter host",
"workflow_node.inspect.form.path.label": "Path",
"workflow_node.inspect.form.path.placeholder": "Please enter path",
"workflow_node.notify.label": "Notification",
"workflow_node.notify.label": "Send notification",
"workflow_node.notify.default_name": "Notification",
"workflow_node.notify.form.subject.label": "Subject",
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
"workflow_node.notify.form.message.label": "Message",
@@ -862,10 +869,13 @@
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
"workflow_node.end.label": "End",
"workflow_node.end.default_name": "End",
"workflow_node.branch.label": "Parallel branch",
"workflow_node.branch.default_name": "Parallel",
"workflow_node.condition.label": "Branch",
"workflow_node.condition.default_name": "Branch",
"workflow_node.condition.form.variable.placeholder": "Please select variable",
"workflow_node.condition.form.variable.errmsg": "Please select variable",
"workflow_node.condition.form.operator.errmsg": "Please select operator",
@@ -888,8 +898,11 @@
"workflow_node.condition.form.comparison.is": "Is",
"workflow_node.execute_result_branch.label": "Execution result branch",
"workflow_node.execute_result_branch.default_name": "Execution result branch",
"workflow_node.execute_success.label": "If the previous node succeeded ...",
"workflow_node.execute_success.default_name": "If the previous node succeeded ...",
"workflow_node.execute_failure.label": "If the previous node failed ..."
"workflow_node.execute_failure.label": "If the previous node failed ...",
"workflow_node.execute_failure.default_name": "If the previous node failed ..."
}

View File

@@ -28,7 +28,7 @@
"access.form.name.placeholder": "请输入授权名称",
"access.form.provider.label": "提供商",
"access.form.provider.placeholder": "请选择提供商",
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
"access.form.provider.search.placeholder": "搜索提供商……",
"access.form.certificate_authority.label": "证书颁发机构",
"access.form.certificate_authority.placeholder": "请选择证书颁发机构",

View File

@@ -10,6 +10,7 @@
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.start.label": "开始",
"workflow_node.start.default_name": "开始",
"workflow_node.start.form.trigger.label": "触发方式",
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
"workflow_node.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。<br>手动触发:手动点击执行触发。",
@@ -22,7 +23,8 @@
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow_node.apply.label": "申请证书",
"workflow_node.apply.label": "申请签发证书",
"workflow_node.apply.default_name": "申请",
"workflow_node.apply.form.domains.label": "域名",
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
@@ -96,7 +98,17 @@
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
"workflow_node.deploy.label": "部署证书",
"workflow_node.upload.label": "上传自有证书",
"workflow_node.upload.default_name": "上传",
"workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate.label": "证书文件PEM 格式)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "私钥文件PEM 格式)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.deploy.label": "部署证书到 ...",
"workflow_node.deploy.default_name": "部署",
"workflow_node.deploy.form.provider.label": "部署目标",
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
"workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……",
@@ -804,25 +816,20 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
"workflow_node.upload.label": "上传证书",
"workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate.label": "证书文件PEM 格式)",
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
"workflow_node.upload.form.private_key.label": "私钥文件PEM 格式)",
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.inspect.label": "检查网站证书",
"workflow_node.inspect.form.domain.label": "域名",
"workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名",
"workflow_node.inspect.form.port.label": "端口号",
"workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号",
"workflow_node.inspect.form.host.label": "Host",
"workflow_node.inspect.form.host.placeholder": "请输入 Host",
"workflow_node.inspect.form.path.label": "Path",
"workflow_node.inspect.form.path.placeholder": "请输入 Path",
"workflow_node.monitor.label": "监控网站证书",
"workflow_node.monitor.default_name": "监控",
"workflow_node.monitor.form.guide": "小贴士Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。",
"workflow_node.monitor.form.host.label": "主机地址",
"workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP",
"workflow_node.monitor.form.port.label": "主机端口",
"workflow_node.monitor.form.port.placeholder": "请输入主机端口",
"workflow_node.monitor.form.domain.label": "域名(可选)",
"workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)",
"workflow_node.monitor.form.request_path.label": "请求路径(可选)",
"workflow_node.monitor.form.request_path.placeholder": "请输入请求路径",
"workflow_node.notify.label": "推送通知",
"workflow_node.notify.default_name": "通知",
"workflow_node.notify.form.subject.label": "通知主题",
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
"workflow_node.notify.form.message.label": "通知内容",
@@ -861,10 +868,13 @@
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
"workflow_node.end.label": "结束",
"workflow_node.end.default_name": "结束",
"workflow_node.branch.label": "并行分支",
"workflow_node.branch.default_name": "并行",
"workflow_node.condition.label": "分支",
"workflow_node.condition.default_name": "分支",
"workflow_node.condition.form.variable.placeholder": "选择变量",
"workflow_node.condition.form.variable.errmsg": "请选择变量",
"workflow_node.condition.form.operator.errmsg": "请选择操作符",
@@ -887,8 +897,11 @@
"workflow_node.condition.form.comparison.is": "为",
"workflow_node.execute_result_branch.label": "执行结果分支",
"workflow_node.execute_result_branch.default_name": "执行结果分支",
"workflow_node.execute_success.label": "若前序节点执行成功…",
"workflow_node.execute_success.default_name": "若前序节点执行成功…",
"workflow_node.execute_failure.label": "若前序节点执行失败…"
"workflow_node.execute_failure.label": "若前序节点执行失败…",
"workflow_node.execute_failure.default_name": "若前序节点执行失败…"
}

View File

@@ -56,7 +56,7 @@ const AccessList = () => {
render: (_, record) => {
return (
<Space className="max-w-full truncate" size={4}>
<Avatar src={accessProvidersMap.get(record.provider)?.icon} size="small" />
<Avatar shape="square" src={accessProvidersMap.get(record.provider)?.icon} size="small" />
<Typography.Text ellipsis>{t(accessProvidersMap.get(record.provider)?.name ?? "")}</Typography.Text>
</Space>
);