diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 6e957d72..ff675f08 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -26,13 +26,16 @@ type Workflow struct { type WorkflowNodeType string const ( - WorkflowNodeTypeStart = WorkflowNodeType("start") - WorkflowNodeTypeEnd = WorkflowNodeType("end") - WorkflowNodeTypeApply = WorkflowNodeType("apply") - WorkflowNodeTypeDeploy = WorkflowNodeType("deploy") - WorkflowNodeTypeNotify = WorkflowNodeType("notify") - WorkflowNodeTypeBranch = WorkflowNodeType("branch") - WorkflowNodeTypeCondition = WorkflowNodeType("condition") + WorkflowNodeTypeStart = WorkflowNodeType("start") + WorkflowNodeTypeEnd = WorkflowNodeType("end") + WorkflowNodeTypeApply = WorkflowNodeType("apply") + WorkflowNodeTypeDeploy = WorkflowNodeType("deploy") + WorkflowNodeTypeNotify = WorkflowNodeType("notify") + WorkflowNodeTypeBranch = WorkflowNodeType("branch") + WorkflowNodeTypeCondition = WorkflowNodeType("condition") + WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch") + WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success") + WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure") ) type WorkflowTriggerType string diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go new file mode 100644 index 00000000..d1ff0034 --- /dev/null +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -0,0 +1,27 @@ +package nodeprocessor + +import ( + "context" + + "github.com/usual2970/certimate/internal/domain" +) + +type executeFailureNode struct { + node *domain.WorkflowNode + *nodeLogger +} + +func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { + return &executeFailureNode{ + node: node, + nodeLogger: NewNodeLogger(node), + } +} + +func (e *executeFailureNode) Run(ctx context.Context) error { + e.AddOutput(ctx, + e.node.Name, + "进入执行失败分支", + ) + return nil +} diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go new file mode 100644 index 00000000..d8d4139f --- /dev/null +++ b/internal/workflow/node-processor/execute_success_node.go @@ -0,0 +1,27 @@ +package nodeprocessor + +import ( + "context" + + "github.com/usual2970/certimate/internal/domain" +) + +type executeSuccessNode struct { + node *domain.WorkflowNode + *nodeLogger +} + +func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { + return &executeSuccessNode{ + node: node, + nodeLogger: NewNodeLogger(node), + } +} + +func (e *executeSuccessNode) Run(ctx context.Context) error { + e.AddOutput(ctx, + e.node.Name, + "进入执行成功分支", + ) + return nil +} diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 1dabd167..3c347815 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -8,7 +8,7 @@ import ( "github.com/usual2970/certimate/internal/domain" ) -type nodeProcessor interface { +type NodeProcessor interface { Run(ctx context.Context) error Log(ctx context.Context) *domain.WorkflowRunLog AddOutput(ctx context.Context, title, content string, err ...string) @@ -58,7 +58,7 @@ func (l *nodeLogger) AddOutput(ctx context.Context, title, content string, err . l.log.Outputs = append(l.log.Outputs, output) } -func GetProcessor(node *domain.WorkflowNode) (nodeProcessor, error) { +func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { switch node.Type { case domain.WorkflowNodeTypeStart: return NewStartNode(node), nil @@ -70,6 +70,10 @@ func GetProcessor(node *domain.WorkflowNode) (nodeProcessor, error) { return NewDeployNode(node), nil case domain.WorkflowNodeTypeNotify: return NewNotifyNode(node), nil + case domain.WorkflowNodeTypeExecuteSuccess: + return NewExecuteSuccessNode(node), nil + case domain.WorkflowNodeTypeExecuteFailure: + return NewExecuteFailureNode(node), nil } return nil, errors.New("not implemented") } diff --git a/internal/workflow/processor/processor.go b/internal/workflow/processor/processor.go index f7c78944..ec6a8da4 100644 --- a/internal/workflow/processor/processor.go +++ b/internal/workflow/processor/processor.go @@ -31,7 +31,7 @@ func (w *workflowProcessor) Run(ctx context.Context) error { func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { current := node for current != nil { - if current.Type == domain.WorkflowNodeTypeBranch { + if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch { for _, branch := range current.Branches { if err := w.processNode(ctx, &branch); err != nil { continue @@ -39,24 +39,37 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl } } - if current.Type != domain.WorkflowNodeTypeBranch { - processor, err := nodes.GetProcessor(current) - if err != nil { - return err - } + var runErr error + var processor nodes.NodeProcessor + for { + if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { + processor, runErr = nodes.GetProcessor(current) + if runErr != nil { + break + } - err = processor.Run(ctx) + runErr = processor.Run(ctx) - log := processor.Log(ctx) - if log != nil { - w.logs = append(w.logs, *log) - } - - if err != nil { - return err + log := processor.Log(ctx) + if log != nil { + w.logs = append(w.logs, *log) + } + if runErr != nil { + break + } } + break + } + + if runErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { + return runErr + } else if runErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { + current = getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) + } else if runErr == nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { + current = getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) + } else { + current = current.Next } - current = current.Next } return nil @@ -65,3 +78,16 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl func setContextWorkflowId(ctx context.Context, id string) context.Context { return context.WithValue(ctx, "workflow_id", id) } + +func GetWorkflowId(ctx context.Context) string { + return ctx.Value("workflow_id").(string) +} + +func getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { + for _, branch := range branches { + if branch.Type == nodeType { + return &branch + } + } + return nil +} diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx index 758699b6..289689dd 100644 --- a/ui/src/components/workflow/WorkflowElement.tsx +++ b/ui/src/components/workflow/WorkflowElement.tsx @@ -7,6 +7,8 @@ import BranchNode from "./node/BranchNode"; import ConditionNode from "./node/ConditionNode"; import DeployNode from "./node/DeployNode"; import EndNode from "./node/EndNode"; +import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode"; +import ExecuteResultNode from "./node/ExecuteResultNode"; import NotifyNode from "./node/NotifyNode"; import StartNode from "./node/StartNode"; @@ -35,6 +37,13 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem case WorkflowNodeType.Branch: return ; + case WorkflowNodeType.ExecuteResultBranch: + return ; + + case WorkflowNodeType.ExecuteSuccess: + case WorkflowNodeType.ExecuteFailure: + return ; + case WorkflowNodeType.Condition: return ; diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index df3f9d1e..a4fa451f 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -27,20 +27,29 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { [WorkflowNodeType.Apply, "workflow_node.apply.label", ], [WorkflowNodeType.Deploy, "workflow_node.deploy.label", ], [WorkflowNodeType.Branch, "workflow_node.branch.label", ], + [WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", ], [WorkflowNodeType.Notify, "workflow_node.notify.label", ], - ].map(([type, label, icon]) => { - return { - key: type as string, - disabled: disabled, - label: t(label as string), - icon: icon, - onClick: () => { - const nextNode = newNode(type as WorkflowNodeType); - addNode(nextNode, node.id); - }, - }; - }); - }, [node.id, disabled]); + ] + .filter(([type]) => { + if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && type === WorkflowNodeType.ExecuteResultBranch) { + return false; + } + + return true; + }) + .map(([type, label, icon]) => { + return { + key: type as string, + disabled: disabled, + label: t(label as string), + icon: icon, + onClick: () => { + const nextNode = newNode(type as WorkflowNodeType); + addNode(nextNode, node.id); + }, + }; + }); + }, [node.id, disabled, node.type]); return (
diff --git a/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx b/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx new file mode 100644 index 00000000..10501cf9 --- /dev/null +++ b/ui/src/components/workflow/node/ExecuteResultBranchNode.tsx @@ -0,0 +1,84 @@ +import { memo } from "react"; +import { theme } from "antd"; + +import { type WorkflowNode } from "@/domain/workflow"; + +import AddNode from "./AddNode"; +import WorkflowElement from "../WorkflowElement"; +import { type SharedNodeProps } from "./_SharedNode"; + +const { useToken } = theme; + +export type BrandNodeProps = SharedNodeProps; + +const ExecuteResultBranchNode = ({ node, disabled }: BrandNodeProps) => { + const token = useToken(); + + const renderBranch = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => { + const elements: JSX.Element[] = []; + + let current = node as typeof node | undefined; + while (current) { + elements.push(); + current = current.next; + } + + return elements; + }; + + return ( + <> +
+ {node.branches?.map((branch, index) => ( +
+ {index == 0 && ( + <> +
+
+ + )} + {node.branches && index == node.branches.length - 1 && ( + <> +
+
+ + )} +
{renderBranch(branch, node.id, index)}
+
+ ))} +
+ + + + ); +}; + +export default memo(ExecuteResultBranchNode); diff --git a/ui/src/components/workflow/node/ExecuteResultNode.tsx b/ui/src/components/workflow/node/ExecuteResultNode.tsx new file mode 100644 index 00000000..a90b7970 --- /dev/null +++ b/ui/src/components/workflow/node/ExecuteResultNode.tsx @@ -0,0 +1,60 @@ +import { memo } from "react"; +import { useTranslation } from "react-i18next"; +import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; +import { Button, Card, Popover } from "antd"; + +import { CheckCircleIcon, XCircleIcon } from "lucide-react"; +import { WorkflowNodeType } from "@/domain/workflow"; +import AddNode from "./AddNode"; +import SharedNode, { type SharedNodeProps } from "./_SharedNode"; + +export type ConditionNodeProps = SharedNodeProps & { + branchId: string; + branchIndex: number; +}; + +const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { + const { t } = useTranslation(); + + return ( + <> + } variant="text" />} + /> + } + overlayClassName="shadow-md" + overlayInnerStyle={{ padding: 0 }} + placement="rightTop" + > + +
+
+ {node.type === WorkflowNodeType.ExecuteSuccess ? ( + <> + +
{t("workflow_node.execute_success.label")}
+ + ) : ( + <> + +
{t("workflow_node.execute_failure.label")}
+ + )} +
+
+
+
+ + + + ); +}; + +export default memo(ExecuteResultNode); diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index 6ecb7da5..868481ef 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -91,7 +91,13 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU }; const handleDeleteClick = async () => { - if (node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition) { + if ( + node.type === WorkflowNodeType.Branch || + node.type === WorkflowNodeType.Condition || + node.type === WorkflowNodeType.ExecuteResultBranch || + node.type === WorkflowNodeType.ExecuteSuccess || + node.type === WorkflowNodeType.ExecuteFailure + ) { await removeBranch(branchId!, branchIndex!); } else { await removeNode(node.id); @@ -151,7 +157,11 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU key: "remove", disabled: disabled || node.type === WorkflowNodeType.Start, label: - node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition + node.type === WorkflowNodeType.Branch || + node.type === WorkflowNodeType.Condition || + node.type === WorkflowNodeType.ExecuteResultBranch || + node.type === WorkflowNodeType.ExecuteSuccess || + node.type === WorkflowNodeType.ExecuteFailure ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"), icon: , diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index d611cd4f..6d35d578 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -30,6 +30,9 @@ export enum WorkflowNodeType { Start = "start", End = "end", Branch = "branch", + ExecuteResultBranch = "execute_result_branch", + ExecuteSuccess = "execute_success", + ExecuteFailure = "execute_failure", Condition = "condition", Apply = "apply", Deploy = "deploy", @@ -41,6 +44,9 @@ const workflowNodeTypeDefaultNames: Map = new Map([ [WorkflowNodeType.Start, i18n.t("workflow_node.start.label")], [WorkflowNodeType.End, i18n.t("workflow_node.end.label")], [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.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.Condition, i18n.t("workflow_node.condition.label")], [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")], [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")], @@ -214,6 +220,17 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} node.branches = [newNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newNode(WorkflowNodeType.Condition, { branchIndex: 1 })]; } break; + case WorkflowNodeType.ExecuteResultBranch: + { + node.branches = [newNode(WorkflowNodeType.ExecuteSuccess), newNode(WorkflowNodeType.ExecuteFailure)]; + } + break; + case WorkflowNodeType.ExecuteSuccess: + case WorkflowNodeType.ExecuteFailure: + { + node.validated = true; + } + break; } return node; @@ -227,7 +244,7 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { Object.assign(current, targetNode); break; } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { current.branches = current.branches!.map((branch) => updateNode(branch, targetNode)); } current = current.next as WorkflowNode; @@ -240,16 +257,16 @@ export const addNode = (node: WorkflowNode, preId: string, targetNode: WorkflowN return produce(node, (draft) => { let current = draft; while (current) { - if (current.id === preId && targetNode.type !== WorkflowNodeType.Branch) { + if (current.id === preId && targetNode.type !== WorkflowNodeType.Branch && targetNode.type !== WorkflowNodeType.ExecuteResultBranch) { targetNode.next = current.next; current.next = targetNode; break; - } else if (current.id === preId && targetNode.type === WorkflowNodeType.Branch) { + } else if (current.id === preId && (targetNode.type === WorkflowNodeType.Branch || targetNode.type === WorkflowNodeType.ExecuteResultBranch)) { targetNode.branches![0].next = current.next; current.next = targetNode; break; } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { current.branches = current.branches!.map((branch) => addNode(branch, preId, targetNode)); } current = current.next as WorkflowNode; @@ -273,7 +290,7 @@ export const addBranch = (node: WorkflowNode, branchNodeId: string) => { ); break; } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { current.branches = current.branches!.map((branch) => addBranch(branch, branchNodeId)); } current = current.next as WorkflowNode; @@ -290,7 +307,7 @@ export const removeNode = (node: WorkflowNode, targetNodeId: string) => { current.next = current.next.next; break; } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { current.branches = current.branches!.map((branch) => removeNode(branch, targetNodeId)); } current = current.next as WorkflowNode; @@ -310,7 +327,7 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }; while (current && last) { if (current.id === branchNodeId) { - if (current.type !== WorkflowNodeType.Branch) { + if (current.type !== WorkflowNodeType.Branch && current.type !== WorkflowNodeType.ExecuteResultBranch) { return draft; } current.branches!.splice(branchIndex, 1); @@ -332,7 +349,7 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd break; } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { current.branches = current.branches!.map((branch) => removeBranch(branch, branchNodeId, branchIndex)); } current = current.next as WorkflowNode; @@ -354,6 +371,11 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: return true; } + // 如果当前节点是execute_failure,清除execute_result_branch节点前一个节点的输出 + if (current.type === WorkflowNodeType.ExecuteFailure) { + output.splice(output.length - 1); + } + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => io.type === type)) { output.push({ ...current, @@ -361,7 +383,7 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: }); } - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { const currentLength = output.length; for (const branch of current.branches!) { if (traverse(branch, output)) { @@ -384,7 +406,7 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: export const isAllNodesValidated = (node: WorkflowNode): boolean => { let current = node as typeof node | undefined; while (current) { - if (current.type === WorkflowNodeType.Branch) { + if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { for (const branch of current.branches!) { if (!isAllNodesValidated(branch)) { return false; diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index efcf2247..f6def82c 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -381,5 +381,11 @@ "workflow_node.branch.label": "Branch", + "workflow_node.execute_result_branch.label": "Execute result branch", + + "workflow_node.execute_success.label": "Execute success", + + "workflow_node.execute_failure.label": "Execute failure", + "workflow_node.condition.label": "Condition" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index ff1732cc..aa82404e 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -381,5 +381,11 @@ "workflow_node.branch.label": "分支", + "workflow_node.execute_result_branch.label": "执行结果分支", + + "workflow_node.execute_success.label": "执行成功", + + "workflow_node.execute_failure.label": "执行失败", + "workflow_node.condition.label": "条件" }