diff --git a/ui/src/components/workflow/WorkflowProvider.tsx b/ui/src/components/workflow/WorkflowProvider.tsx
index 1a44d266..1b341f06 100644
--- a/ui/src/components/workflow/WorkflowProvider.tsx
+++ b/ui/src/components/workflow/WorkflowProvider.tsx
@@ -1,6 +1,4 @@
-import React from "react";
-
-import { PanelProvider } from "./PanelProvider";
+import { PanelProvider } from "./panel/PanelProvider";
const WorkflowProvider = ({ children }: { children: React.ReactNode }) => {
return
{children};
diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx
index f01532de..39e2b2fb 100644
--- a/ui/src/components/workflow/node/AddNode.tsx
+++ b/ui/src/components/workflow/node/AddNode.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import { memo, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
CloudUploadOutlined as CloudUploadOutlinedIcon,
@@ -43,7 +43,7 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
},
};
});
- }, []);
+ }, [node.id, disabled]);
return (
@@ -56,4 +56,4 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
);
};
-export default AddNode;
+export default memo(AddNode);
diff --git a/ui/src/components/workflow/node/ApplyNodeForm.tsx b/ui/src/components/workflow/node/ApplyNodeForm.tsx
index 6b4523ae..1d365f72 100644
--- a/ui/src/components/workflow/node/ApplyNodeForm.tsx
+++ b/ui/src/components/workflow/node/ApplyNodeForm.tsx
@@ -2,9 +2,8 @@ import { memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { useControllableValue } from "ahooks";
-import { AutoComplete, type AutoCompleteProps, Button, Divider, Form, Input, Select, Space, Switch, Tooltip, Typography } from "antd";
+import { AutoComplete, type AutoCompleteProps, Button, Divider, Form, type FormInstance, Input, Select, Space, Switch, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod";
-import { produce } from "immer";
import { z } from "zod";
import ModalForm from "@/components/ModalForm";
@@ -13,20 +12,22 @@ import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForApply } from "@/domain/workflow";
-import { useAntdForm, useZustandShallowSelector } from "@/hooks";
-import { useAccessesStore } from "@/stores/access";
import { useContactEmailsStore } from "@/stores/contact";
-import { useWorkflowStore } from "@/stores/workflow";
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
-import { usePanel } from "../PanelProvider";
+
+type ApplyNodeFormFieldValues = Partial
;
export type ApplyNodeFormProps = {
- node: WorkflowNode;
+ form: FormInstance;
+ formName?: string;
+ disabled?: boolean;
+ workflowNode: WorkflowNode;
+ onValuesChange?: (values: ApplyNodeFormFieldValues) => void;
};
const MULTIPLE_INPUT_DELIMITER = ";";
-const initFormModel = (): Partial => {
+const initFormModel = (): ApplyNodeFormFieldValues => {
return {
keyAlgorithm: "RSA2048",
propagationTimeout: 60,
@@ -34,21 +35,16 @@ const initFormModel = (): Partial => {
};
};
-const ApplyNodeForm = ({ node }: ApplyNodeFormProps) => {
+const ApplyNodeForm = ({ form, formName, disabled, workflowNode, onValuesChange }: ApplyNodeFormProps) => {
const { t } = useTranslation();
- const { accesses } = useAccessesStore(useZustandShallowSelector("accesses"));
- const { addEmail } = useContactEmailsStore(useZustandShallowSelector("addEmail"));
- const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
- const { hidePanel } = usePanel();
-
const formSchema = z.object({
domains: 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")),
- contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email("common.errmsg.email_invalid"),
+ contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")),
providerAccessId: z
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
.min(1, t("workflow_node.apply.form.provider_access.placeholder")),
@@ -73,46 +69,27 @@ const ApplyNodeForm = ({ node }: ApplyNodeFormProps) => {
disableFollowCNAME: z.boolean().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
- const {
- form: formInst,
- formPending,
- formProps,
- } = useAntdForm>({
- name: "workflowApplyNodeForm",
- initialValues: (node?.config as WorkflowNodeConfigForApply) ?? initFormModel(),
- onSubmit: async (values) => {
- await formInst.validateFields();
- await addEmail(values.contactEmail);
- await updateNode(
- produce(node, (draft) => {
- draft.config = {
- provider: accesses.find((e) => e.id === values.providerAccessId)?.provider,
- ...values,
- } as WorkflowNodeConfigForApply;
- draft.validated = true;
- })
- );
- hidePanel();
- },
- });
- const [fieldDomains, setFieldDomains] = useState(node?.config?.domains as string);
- const [fieldNameservers, setFieldNameservers] = useState(node?.config?.nameservers as string);
+ const initialValues: ApplyNodeFormFieldValues = (workflowNode.config as WorkflowNodeConfigForApply) ?? initFormModel();
- const handleFieldDomainsChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- setFieldDomains(value);
- formInst.setFieldValue("domains", value);
- };
+ const fieldDomains = Form.useWatch("domains", form);
+ const fieldNameservers = Form.useWatch("nameservers", form);
- const handleFieldNameserversChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- setFieldNameservers(value);
- formInst.setFieldValue("nameservers", value);
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values as ApplyNodeFormFieldValues);
};
return (
- {
>
{
+ form.setFieldValue("domains", e.target.value);
+ }}
/>
+
}
onFinish={(v) => {
- setFieldDomains(v);
- formInst.setFieldValue("domains", v);
+ form.setFieldValue("domains", v);
}}
/>
@@ -173,7 +151,7 @@ const ApplyNodeForm = ({ node }: ApplyNodeFormProps) => {
onSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage) {
- formInst.setFieldValue("providerAccessId", record.id);
+ form.setFieldValue("providerAccessId", record.id);
}
}}
/>
@@ -216,21 +194,22 @@ const ApplyNodeForm = ({ node }: ApplyNodeFormProps) => {
{
+ form.setFieldValue("nameservers", e.target.value);
+ }}
/>
+
}
onFinish={(v) => {
- setFieldNameservers(v);
- formInst.setFieldValue("nameservers", v);
+ form.setFieldValue("nameservers", v);
}}
/>
@@ -260,12 +239,6 @@ const ApplyNodeForm = ({ node }: ApplyNodeFormProps) => {
>
-
-
-
-
);
};
@@ -340,7 +313,7 @@ const FormFieldDomainsModalForm = ({
trigger,
onFinish,
}: {
- data: string;
+ data?: string;
disabled?: boolean;
trigger?: React.ReactNode;
onFinish?: (data: string) => void;
@@ -353,7 +326,7 @@ const FormFieldDomainsModalForm = ({
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
- const [formInst] = Form.useForm>();
+ const [form] = Form.useForm>();
const [model, setModel] = useState>>({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
useEffect(() => {
@@ -372,7 +345,7 @@ const FormFieldDomainsModalForm = ({
return (
void }) => {
+const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data?: string; trigger?: React.ReactNode; onFinish?: (data: string) => void }) => {
const { t } = useTranslation();
const formSchema = z.object({
@@ -397,7 +370,7 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
- const [formInst] = Form.useForm>();
+ const [form] = Form.useForm>();
const [model, setModel] = useState>>({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
useEffect(() => {
@@ -416,7 +389,7 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
return (
{
const { addBranch } = useWorkflowStore(useZustandShallowSelector(["addBranch"]));
- const renderNodes = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => {
+ const renderBranch = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => {
const elements: JSX.Element[] = [];
- let current = node as WorkflowNode | undefined;
+ let current = node as typeof node | undefined;
while (current) {
- elements.push();
+ elements.push();
current = current.next;
}
@@ -47,12 +47,12 @@ const BranchNode = ({ node, disabled }: BrandNodeProps) => {
{t("workflow_node.action.add_branch")}
- {node.branches!.map((branch, index) => (
+ {node.branches?.map((branch, index) => (
-
{renderNodes(branch, node.id, index)}
+
{renderBranch(branch, node.id, index)}
))}
diff --git a/ui/src/components/workflow/node/CommonNode.tsx b/ui/src/components/workflow/node/CommonNode.tsx
new file mode 100644
index 00000000..35c485ec
--- /dev/null
+++ b/ui/src/components/workflow/node/CommonNode.tsx
@@ -0,0 +1,240 @@
+import { memo, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { CloseCircleOutlined as CloseCircleOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon } from "@ant-design/icons";
+import { Avatar, Button, Card, Dropdown, Popover, Space, Typography } from "antd";
+import { produce } from "immer";
+
+import Show from "@/components/Show";
+import { deployProvidersMap } from "@/domain/provider";
+import { notifyChannelsMap } from "@/domain/settings";
+import {
+ WORKFLOW_TRIGGERS,
+ type WorkflowNode,
+ type WorkflowNodeConfigForApply,
+ type WorkflowNodeConfigForDeploy,
+ type WorkflowNodeConfigForNotify,
+ type WorkflowNodeConfigForStart,
+ WorkflowNodeType,
+} from "@/domain/workflow";
+import { useAntdForm, useZustandShallowSelector } from "@/hooks";
+import { useAccessesStore } from "@/stores/access";
+import { useContactEmailsStore } from "@/stores/contact";
+import { useWorkflowStore } from "@/stores/workflow";
+
+import AddNode from "./AddNode";
+import ApplyNodeForm from "./ApplyNodeForm";
+import DeployNodeForm from "./DeployNodeForm";
+import NotifyNodeForm from "./NotifyNodeForm";
+import StartNodeForm from "./StartNodeForm";
+import { usePanelContext } from "../panel/PanelContext";
+
+export type CommonNodeProps = {
+ node: WorkflowNode;
+ disabled?: boolean;
+};
+
+const CommonNode = ({ node, disabled }: CommonNodeProps) => {
+ const { t } = useTranslation();
+
+ const { accesses } = useAccessesStore(useZustandShallowSelector("accesses"));
+ const { addEmail } = useContactEmailsStore(useZustandShallowSelector(["addEmail"]));
+ const { updateNode, removeNode } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeNode"]));
+ const { confirm: confirmPanel } = usePanelContext();
+
+ const {
+ form: formInst,
+ formPending,
+ formProps,
+ submit: submitForm,
+ } = useAntdForm({
+ name: "workflowNodeForm",
+ onSubmit: async (values) => {
+ if (node.type === WorkflowNodeType.Apply) {
+ await addEmail(values.contactEmail);
+ await updateNode(
+ produce(node, (draft) => {
+ draft.config = {
+ provider: accesses.find((e) => e.id === values.providerAccessId)?.provider,
+ ...values,
+ };
+ draft.validated = true;
+ })
+ );
+ } else {
+ await updateNode(
+ produce(node, (draft) => {
+ draft.config = { ...values };
+ draft.validated = true;
+ })
+ );
+ }
+ },
+ });
+
+ const nodeContentComponent = useMemo(() => {
+ if (!node.validated) {
+ return
{t("workflow_node.action.configure_node")};
+ }
+
+ switch (node.type) {
+ case WorkflowNodeType.Start: {
+ const config = (node.config as WorkflowNodeConfigForStart) ?? {};
+ return (
+
+
+ {config.trigger === WORKFLOW_TRIGGERS.AUTO
+ ? t("workflow.props.trigger.auto")
+ : config.trigger === WORKFLOW_TRIGGERS.MANUAL
+ ? t("workflow.props.trigger.manual")
+ : " "}
+
+
+ {config.trigger === WORKFLOW_TRIGGERS.AUTO ? config.triggerCron : ""}
+
+
+ );
+ }
+
+ case WorkflowNodeType.Apply: {
+ const config = (node.config as WorkflowNodeConfigForApply) ?? {};
+ return
{config.domains || " "};
+ }
+
+ case WorkflowNodeType.Deploy: {
+ const config = (node.config as WorkflowNodeConfigForDeploy) ?? {};
+ const provider = deployProvidersMap.get(config.provider);
+ return (
+
+
+ {t(provider?.name ?? "")}
+
+ );
+ }
+
+ case WorkflowNodeType.Notify: {
+ const config = (node.config as WorkflowNodeConfigForNotify) ?? {};
+ const channel = notifyChannelsMap.get(config.channel as string);
+ return (
+
+ {t(channel?.name ?? " ")}
+
+ {config.subject ?? ""}
+
+
+ );
+ }
+
+ default: {
+ console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
+ return <>>;
+ }
+ }
+ }, [node]);
+
+ const panelBodyComponent = useMemo(() => {
+ const nodeFormProps = {
+ form: formInst,
+ formName: formProps.name,
+ disabled: disabled || formPending,
+ workflowNode: node,
+ };
+
+ switch (node.type) {
+ case WorkflowNodeType.Start:
+ return
;
+ case WorkflowNodeType.Apply:
+ return
;
+ case WorkflowNodeType.Deploy:
+ return
;
+ case WorkflowNodeType.Notify:
+ return
;
+ default:
+ console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
+ return <> >;
+ }
+ }, [node, disabled, formInst, formPending, formProps]);
+
+ const handleNodeClick = () => {
+ confirmPanel({
+ title: node.name,
+ children: panelBodyComponent,
+ okText: t("common.button.save"),
+ onOk: () => {
+ return submitForm();
+ },
+ });
+ };
+
+ const handleNodeNameBlur = (e: React.FocusEvent
) => {
+ const oldName = node.name;
+ const newName = e.target.innerText.trim();
+ if (oldName === newName) {
+ return;
+ }
+
+ updateNode(
+ produce(node, (draft) => {
+ draft.name = newName;
+ })
+ );
+ };
+
+ return (
+ <>
+
+ ,
+ danger: true,
+ onClick: () => {
+ if (disabled) return;
+
+ removeNode(node.id);
+ },
+ },
+ ],
+ }}
+ trigger={["click"]}
+ >
+ } variant="text" />
+
+
+ }
+ overlayClassName="shadow-md"
+ overlayInnerStyle={{ padding: 0 }}
+ placement="rightTop"
+ >
+
+
+
+
+
+ {nodeContentComponent}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default memo(CommonNode);
diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx
index e1bf21d3..5edd80d7 100644
--- a/ui/src/components/workflow/node/ConditionNode.tsx
+++ b/ui/src/components/workflow/node/ConditionNode.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react";
import { useTranslation } from "react-i18next";
import { CloseCircleOutlined as CloseCircleOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon } from "@ant-design/icons";
import { Button, Card, Dropdown, Popover } from "antd";
@@ -85,4 +86,4 @@ const ConditionNode = ({ node, branchId, branchIndex, disabled }: ConditionNodeP
);
};
-export default ConditionNode;
+export default memo(ConditionNode);
diff --git a/ui/src/components/workflow/node/DeployNodeForm.tsx b/ui/src/components/workflow/node/DeployNodeForm.tsx
index eccc6a46..7e278ff4 100644
--- a/ui/src/components/workflow/node/DeployNodeForm.tsx
+++ b/ui/src/components/workflow/node/DeployNodeForm.tsx
@@ -1,9 +1,9 @@
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
-import { Button, Divider, Form, Select, Tooltip, Typography } from "antd";
+import { Button, Divider, Form, type FormInstance, Select, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod";
-import { produce } from "immer";
+import { init } from "i18next";
import { z } from "zod";
import Show from "@/components/Show";
@@ -13,9 +13,9 @@ import DeployProviderPicker from "@/components/provider/DeployProviderPicker";
import DeployProviderSelect from "@/components/provider/DeployProviderSelect";
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
-import { useAntdForm, useZustandShallowSelector } from "@/hooks";
+import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
-import { usePanel } from "../PanelProvider";
+
import DeployNodeFormAliyunALBFields from "./DeployNodeFormAliyunALBFields";
import DeployNodeFormAliyunCDNFields from "./DeployNodeFormAliyunCDNFields";
import DeployNodeFormAliyunCLBFields from "./DeployNodeFormAliyunCLBFields";
@@ -40,19 +40,30 @@ import DeployNodeFormVolcEngineCDNFields from "./DeployNodeFormVolcEngineCDNFiel
import DeployNodeFormVolcEngineLiveFields from "./DeployNodeFormVolcEngineLiveFields";
import DeployNodeFormWebhookFields from "./DeployNodeFormWebhookFields";
+type DeployNodeFormFieldValues = Partial;
+
export type DeployFormProps = {
- node: WorkflowNode;
+ form: FormInstance;
+ formName?: string;
+ disabled?: boolean;
+ workflowNode: WorkflowNode;
+ onValuesChange?: (values: DeployNodeFormFieldValues) => void;
};
-const initFormModel = (): Partial => {
+const initFormModel = (): DeployNodeFormFieldValues => {
return {};
};
-const DeployNodeForm = ({ node }: DeployFormProps) => {
+const DeployNodeForm = ({ form, formName, disabled, workflowNode, onValuesChange }: DeployFormProps) => {
const { t } = useTranslation();
- const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
- const { hidePanel } = usePanel();
+ const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
+
+ const [previousOutput, setPreviousOutput] = useState([]);
+ useEffect(() => {
+ const rs = getWorkflowOuptutBeforeId(workflowNode.id, "certificate");
+ setPreviousOutput(rs);
+ }, [workflowNode.id, getWorkflowOuptutBeforeId]);
const formSchema = z.object({
provider: z.string({ message: t("workflow_node.deploy.form.provider.placeholder") }).nonempty(t("workflow_node.deploy.form.provider.placeholder")),
@@ -62,32 +73,10 @@ const DeployNodeForm = ({ node }: DeployFormProps) => {
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>({
- name: "workflowDeployNodeForm",
- initialValues: (node?.config as WorkflowNodeConfigForDeploy) ?? initFormModel(),
- onSubmit: async (values) => {
- await formInst.validateFields();
- await updateNode(
- produce(node, (draft) => {
- draft.config = { ...values };
- draft.validated = true;
- })
- );
- hidePanel();
- },
- });
- const [previousOutput, setPreviousOutput] = useState([]);
- useEffect(() => {
- const rs = getWorkflowOuptutBeforeId(node.id, "certificate");
- setPreviousOutput(rs);
- }, [node, getWorkflowOuptutBeforeId]);
+ const initialValues: DeployNodeFormFieldValues = (workflowNode.config as WorkflowNodeConfigForDeploy) ?? initFormModel();
- const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true });
+ const fieldProvider = Form.useWatch("provider", { form: form, preserve: true });
const formFieldsComponent = useMemo(() => {
/*
@@ -146,9 +135,9 @@ const DeployNodeForm = ({ node }: DeployFormProps) => {
const handleProviderPick = useCallback(
(value: string) => {
- formInst.setFieldValue("provider", value);
+ form.setFieldValue("provider", value);
},
- [formInst]
+ [form]
);
const handleProviderSelect = (value: string) => {
@@ -156,10 +145,10 @@ const DeployNodeForm = ({ node }: DeployFormProps) => {
// TODO: 暂时不支持切换部署目标,需后端调整,否则之前若存在部署结果输出就不会再部署
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
- if (node.config?.provider === value) {
- formInst.resetFields();
+ if (initialValues?.provider === value) {
+ form.resetFields();
} else {
- const oldValues = formInst.getFieldsValue();
+ const oldValues = form.getFieldsValue();
const newValues: Record = {};
for (const key in oldValues) {
if (key === "provider" || key === "providerAccessId" || key === "certificate") {
@@ -168,16 +157,29 @@ const DeployNodeForm = ({ node }: DeployFormProps) => {
newValues[key] = undefined;
}
}
- formInst.setFieldsValue(newValues);
+ form.setFieldsValue(newValues);
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value)?.provider) {
- formInst.setFieldValue("providerAccessId", undefined);
+ form.setFieldValue("providerAccessId", undefined);
}
}
};
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values as DeployNodeFormFieldValues);
+ };
+
return (
-
{
onSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.DEPLOY === provider?.usage) {
- formInst.setFieldValue("providerAccessId", record.id);
+ form.setFieldValue("providerAccessId", record.id);
}
}}
/>
@@ -264,12 +266,6 @@ const DeployNodeForm = ({ node }: DeployFormProps) => {
{formFieldsComponent}
-
-
-
-
);
diff --git a/ui/src/components/workflow/node/EndNode.tsx b/ui/src/components/workflow/node/EndNode.tsx
index 9700a847..de419bcf 100644
--- a/ui/src/components/workflow/node/EndNode.tsx
+++ b/ui/src/components/workflow/node/EndNode.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react";
import { useTranslation } from "react-i18next";
import { Typography } from "antd";
@@ -14,4 +15,4 @@ const EndNode = () => {
);
};
-export default EndNode;
+export default memo(EndNode);
diff --git a/ui/src/components/workflow/node/NodeRender.tsx b/ui/src/components/workflow/node/NodeRender.tsx
deleted file mode 100644
index d64648a4..00000000
--- a/ui/src/components/workflow/node/NodeRender.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { memo } from "react";
-
-import { type WorkflowNode, WorkflowNodeType } from "@/domain/workflow";
-
-import WorkflowElement from "../WorkflowElement";
-import BranchNode from "./BranchNode";
-import ConditionNode from "./ConditionNode";
-import EndNode from "./EndNode";
-
-export type NodeRenderProps = {
- node: WorkflowNode;
- branchId?: string;
- branchIndex?: number;
- disabled?: boolean;
-};
-
-const NodeRender = ({ node: data, branchId, branchIndex, disabled }: NodeRenderProps) => {
- const render = () => {
- switch (data.type) {
- case WorkflowNodeType.Start:
- case WorkflowNodeType.Apply:
- case WorkflowNodeType.Deploy:
- case WorkflowNodeType.Notify:
- return ;
- case WorkflowNodeType.End:
- return ;
- case WorkflowNodeType.Branch:
- return ;
- case WorkflowNodeType.Condition:
- return ;
- }
- };
-
- return <>{render()}>;
-};
-
-export default memo(NodeRender);
diff --git a/ui/src/components/workflow/node/NotifyNodeForm.tsx b/ui/src/components/workflow/node/NotifyNodeForm.tsx
index d1256079..aa8dcf6d 100644
--- a/ui/src/components/workflow/node/NotifyNodeForm.tsx
+++ b/ui/src/components/workflow/node/NotifyNodeForm.tsx
@@ -2,30 +2,33 @@ import { memo, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
-import { Button, Form, Input, Select } from "antd";
+import { Button, Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
-import { produce } from "immer";
import { z } from "zod";
import { notifyChannelsMap } from "@/domain/settings";
import { type WorkflowNode, type WorkflowNodeConfigForNotify } from "@/domain/workflow";
-import { useAntdForm, useZustandShallowSelector } from "@/hooks";
+import { useZustandShallowSelector } from "@/hooks";
import { useNotifyChannelsStore } from "@/stores/notify";
-import { useWorkflowStore } from "@/stores/workflow";
-import { usePanel } from "../PanelProvider";
+
+type NotifyNodeFormFieldValues = Partial;
export type NotifyNodeFormProps = {
- node: WorkflowNode;
+ form: FormInstance;
+ formName?: string;
+ disabled?: boolean;
+ workflowNode: WorkflowNode;
+ onValuesChange?: (values: NotifyNodeFormFieldValues) => void;
};
-const initFormModel = (): Partial => {
+const initFormModel = (): NotifyNodeFormFieldValues => {
return {
subject: "Completed!",
message: "Your workflow has been completed on Certimate.",
};
};
-const NotifyNodeForm = ({ node }: NotifyNodeFormProps) => {
+const NotifyNodeForm = ({ form, formName, disabled, workflowNode, onValuesChange }: NotifyNodeFormProps) => {
const { t } = useTranslation();
const {
@@ -37,9 +40,6 @@ const NotifyNodeForm = ({ node }: NotifyNodeFormProps) => {
fetchChannels();
}, [fetchChannels]);
- const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
- const { hidePanel } = usePanel();
-
const formSchema = z.object({
subject: z
.string({ message: t("workflow_node.notify.form.subject.placeholder") })
@@ -52,27 +52,24 @@ const NotifyNodeForm = ({ node }: NotifyNodeFormProps) => {
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 {
- form: formInst,
- formPending,
- formProps,
- } = useAntdForm>({
- name: "workflowNotifyNodeForm",
- initialValues: (node?.config as WorkflowNodeConfigForNotify) ?? initFormModel(),
- onSubmit: async (values) => {
- await formInst.validateFields();
- await updateNode(
- produce(node, (draft) => {
- draft.config = { ...values };
- draft.validated = true;
- })
- );
- hidePanel();
- },
- });
+
+ const initialValues: NotifyNodeFormFieldValues = (workflowNode.config as WorkflowNodeConfigForNotify) ?? initFormModel();
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values as NotifyNodeFormFieldValues);
+ };
return (
-
@@ -108,12 +105,6 @@ const NotifyNodeForm = ({ node }: NotifyNodeFormProps) => {
/>
-
-
-
-
);
};
diff --git a/ui/src/components/workflow/node/StartNodeForm.tsx b/ui/src/components/workflow/node/StartNodeForm.tsx
index a1dcdd71..1e8ba2b8 100644
--- a/ui/src/components/workflow/node/StartNodeForm.tsx
+++ b/ui/src/components/workflow/node/StartNodeForm.tsx
@@ -1,35 +1,34 @@
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { Alert, Button, Form, Input, Radio } from "antd";
+import { Alert, Form, type FormInstance, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import dayjs from "dayjs";
-import { produce } from "immer";
import { z } from "zod";
import Show from "@/components/Show";
-import { WORKFLOW_TRIGGERS, type WorkflowNode, type WorkflowNodeConfigForStart } from "@/domain/workflow";
-import { useAntdForm, useZustandShallowSelector } from "@/hooks";
-import { useWorkflowStore } from "@/stores/workflow";
+import { WORKFLOW_TRIGGERS, type WorkflowNode, type WorkflowNodeConfigForStart, type WorkflowTriggerType } from "@/domain/workflow";
import { getNextCronExecutions, validCronExpression } from "@/utils/cron";
-import { usePanel } from "../PanelProvider";
+
+type StartNodeFormFieldValues = Partial;
export type StartNodeFormProps = {
- node: WorkflowNode;
+ form: FormInstance;
+ formName?: string;
+ disabled?: boolean;
+ workflowNode: WorkflowNode;
+ onValuesChange?: (values: StartNodeFormFieldValues) => void;
};
-const initFormModel = (): WorkflowNodeConfigForStart => {
+const initFormModel = (): StartNodeFormFieldValues => {
return {
trigger: WORKFLOW_TRIGGERS.AUTO,
triggerCron: "0 0 * * *",
};
};
-const StartNodeForm = ({ node }: StartNodeFormProps) => {
+const StartNodeForm = ({ form, formName, disabled, workflowNode, onValuesChange }: StartNodeFormProps) => {
const { t } = useTranslation();
- const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
- const { hidePanel } = usePanel();
-
const formSchema = z
.object({
trigger: z.string({ message: t("workflow_node.start.form.trigger.placeholder") }).min(1, t("workflow_node.start.form.trigger.placeholder")),
@@ -49,27 +48,11 @@ const StartNodeForm = ({ node }: StartNodeFormProps) => {
}
});
const formRule = createSchemaFieldRule(formSchema);
- const {
- form: formInst,
- formPending,
- formProps,
- } = useAntdForm>({
- name: "workflowStartNodeForm",
- initialValues: (node?.config as WorkflowNodeConfigForStart) ?? initFormModel(),
- onSubmit: async (values) => {
- await formInst.validateFields();
- await updateNode(
- produce(node, (draft) => {
- draft.config = { ...values };
- draft.validated = true;
- })
- );
- hidePanel();
- },
- });
- const fieldTrigger = Form.useWatch("trigger", formInst);
- const fieldTriggerCron = Form.useWatch("triggerCron", formInst);
+ const initialValues: StartNodeFormFieldValues = (workflowNode.config as WorkflowNodeConfigForStart) ?? initFormModel();
+
+ const fieldTrigger = Form.useWatch("trigger", form);
+ const fieldTriggerCron = Form.useWatch("triggerCron", form);
const [fieldTriggerCronExpectedExecutions, setFieldTriggerCronExpectedExecutions] = useState([]);
useEffect(() => {
setFieldTriggerCronExpectedExecutions(getNextCronExecutions(fieldTriggerCron, 5));
@@ -77,12 +60,27 @@ const StartNodeForm = ({ node }: StartNodeFormProps) => {
const handleTriggerChange = (value: string) => {
if (value === WORKFLOW_TRIGGERS.AUTO) {
- formInst.setFieldValue("triggerCron", formInst.getFieldValue("triggerCron") || initFormModel().triggerCron);
+ form.setFieldValue("triggerCron", initialValues.triggerCron || initFormModel().triggerCron);
+ } else {
+ form.setFieldValue("triggerCron", undefined);
}
};
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values as StartNodeFormFieldValues);
+ };
+
return (
- {
} />
-
-
-
-
);
};
diff --git a/ui/src/components/workflow/panel/Panel.tsx b/ui/src/components/workflow/panel/Panel.tsx
new file mode 100644
index 00000000..dd6d3eb9
--- /dev/null
+++ b/ui/src/components/workflow/panel/Panel.tsx
@@ -0,0 +1,41 @@
+import React, { useEffect } from "react";
+import { useControllableValue } from "ahooks";
+import { Drawer } from "antd";
+
+export type PanelProps = {
+ children: React.ReactNode;
+ defaultOpen?: boolean;
+ extra?: React.ReactNode;
+ footer?: React.ReactNode;
+ open?: boolean;
+ title?: React.ReactNode;
+ onClose?: () => void | Promise;
+ onOpenChange?: (open: boolean) => void;
+};
+
+const Panel = ({ children, extra, footer, title, onClose, ...props }: PanelProps) => {
+ const [open, setOpen] = useControllableValue(props, {
+ valuePropName: "open",
+ defaultValuePropName: "defaultOpen",
+ trigger: "onOpenChange",
+ });
+
+ const handleClose = async () => {
+ try {
+ const ret = await onClose?.();
+ if (ret != null && !ret) return;
+
+ setOpen(false);
+ } catch {
+ return;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Panel;
diff --git a/ui/src/components/workflow/panel/PanelContext.ts b/ui/src/components/workflow/panel/PanelContext.ts
new file mode 100644
index 00000000..bc5a46a8
--- /dev/null
+++ b/ui/src/components/workflow/panel/PanelContext.ts
@@ -0,0 +1,34 @@
+import { createContext, useContext } from "react";
+import { type ButtonProps } from "antd";
+
+import { type PanelProps } from "./Panel";
+
+export type ShowPanelOptions = Omit;
+export type ShowPanelWithConfirmOptions = Omit & {
+ cancelButtonProps?: ButtonProps;
+ cancelText?: React.ReactNode;
+ okButtonProps?: ButtonProps;
+ okText?: React.ReactNode;
+ onCancel?: () => void;
+ onOk?: () => void | Promise;
+};
+
+export type PanelContextProps = {
+ open: boolean;
+ show: (options: ShowPanelOptions) => void;
+ confirm: (options: ShowPanelWithConfirmOptions) => void;
+ hide: () => void;
+};
+
+const PanelContext = createContext(undefined);
+
+export const usePanelContext = () => {
+ const context = useContext(PanelContext);
+ if (!context) {
+ throw new Error("`usePanelContext` must be used within `PanelProvider`");
+ }
+
+ return context;
+};
+
+export default PanelContext;
diff --git a/ui/src/components/workflow/panel/PanelProvider.tsx b/ui/src/components/workflow/panel/PanelProvider.tsx
new file mode 100644
index 00000000..16a87c69
--- /dev/null
+++ b/ui/src/components/workflow/panel/PanelProvider.tsx
@@ -0,0 +1,90 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Button, Space } from "antd";
+
+import Panel from "./Panel";
+import PanelContext, { type ShowPanelOptions, type ShowPanelWithConfirmOptions } from "./PanelContext";
+
+export const PanelProvider = ({ children }: { children: React.ReactNode }) => {
+ const { t } = useTranslation();
+
+ const [open, setOpen] = useState(false);
+ const [options, setOptions] = useState();
+
+ const showPanel = (options: ShowPanelOptions) => {
+ setOpen(true);
+ setOptions(options);
+ };
+
+ const showPanelWithConfirm = (options: ShowPanelWithConfirmOptions) => {
+ const updateOptionsFooter = (confirmLoading: boolean) => {
+ setOptions({
+ ...options,
+ footer: (
+
+
+
+
+ ),
+ onClose: () => Promise.resolve(!confirmLoading),
+ });
+ };
+
+ showPanel(options);
+ updateOptionsFooter(false);
+ };
+
+ const hidePanel = () => {
+ setOpen(false);
+ setOptions(undefined);
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ setOpen(open);
+
+ if (!open) {
+ setOptions(undefined);
+ }
+ };
+
+ return (
+
+ {children}
+
+
+ {options?.children}
+
+
+ );
+};