mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 05:29:51 +00:00
473 lines
12 KiB
TypeScript
473 lines
12 KiB
TypeScript
import { produce } from "immer";
|
|
import { nanoid } from "nanoid";
|
|
import i18n from "@/i18n";
|
|
import { deployTargets, KVType } from "./domain";
|
|
|
|
export type WorkflowRunLog = {
|
|
id: string;
|
|
workflow: string;
|
|
log: WorkflowRunLogItem[];
|
|
error: string;
|
|
succeed: boolean;
|
|
created: string;
|
|
updated: string;
|
|
};
|
|
|
|
export type WorkflowRunLogItem = {
|
|
nodeName: string;
|
|
error: string;
|
|
outputs: WorkflowOutput[];
|
|
};
|
|
|
|
export type WorkflowOutput = {
|
|
time: string;
|
|
title: string;
|
|
content: string;
|
|
error: string;
|
|
};
|
|
|
|
export type Workflow = {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
type: string;
|
|
crontab?: string;
|
|
content?: WorkflowNode;
|
|
draft?: WorkflowNode;
|
|
enabled?: boolean;
|
|
hasDraft?: boolean;
|
|
created?: string;
|
|
updated?: string;
|
|
};
|
|
|
|
export enum WorkflowNodeType {
|
|
Start = "start",
|
|
End = "end",
|
|
Branch = "branch",
|
|
Condition = "condition",
|
|
Apply = "apply",
|
|
Deploy = "deploy",
|
|
Notify = "notify",
|
|
Custom = "custom",
|
|
}
|
|
|
|
const i18nPrefix = "workflow.node";
|
|
|
|
export const workflowNodeTypeDefaultName: Map<WorkflowNodeType, string> = new Map([
|
|
[WorkflowNodeType.Start, i18n.t(`${i18nPrefix}.start.title`)],
|
|
[WorkflowNodeType.End, i18n.t(`${i18nPrefix}.end.title`)],
|
|
[WorkflowNodeType.Branch, i18n.t(`${i18nPrefix}.branch.title`)],
|
|
[WorkflowNodeType.Condition, i18n.t(`${i18nPrefix}.condition.title`)],
|
|
[WorkflowNodeType.Apply, i18n.t(`${i18nPrefix}.apply.title`)],
|
|
[WorkflowNodeType.Deploy, i18n.t(`${i18nPrefix}.deploy.title`)],
|
|
[WorkflowNodeType.Notify, i18n.t(`${i18nPrefix}.notify.title`)],
|
|
[WorkflowNodeType.Custom, i18n.t(`${i18nPrefix}.custom.title`)],
|
|
]);
|
|
|
|
export type WorkflowNodeIo = {
|
|
name: string;
|
|
type: string;
|
|
required: boolean;
|
|
label: string;
|
|
value?: string;
|
|
valueSelector?: WorkflowNodeIoValueSelector;
|
|
};
|
|
|
|
export type WorkflowNodeIoValueSelector = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
export const workflowNodeTypeDefaultInput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([
|
|
[WorkflowNodeType.Apply, []],
|
|
[
|
|
WorkflowNodeType.Deploy,
|
|
[
|
|
{
|
|
name: "certificate",
|
|
type: " certificate",
|
|
required: true,
|
|
label: i18n.t("workflow.common.certificate.label"),
|
|
},
|
|
],
|
|
],
|
|
[WorkflowNodeType.Notify, []],
|
|
]);
|
|
|
|
export const workflowNodeTypeDefaultOutput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([
|
|
[
|
|
WorkflowNodeType.Apply,
|
|
[
|
|
{
|
|
name: "certificate",
|
|
type: "certificate",
|
|
required: true,
|
|
label: i18n.t("workflow.common.certificate.label"),
|
|
},
|
|
],
|
|
],
|
|
[WorkflowNodeType.Deploy, []],
|
|
[WorkflowNodeType.Notify, []],
|
|
]);
|
|
|
|
export type WorkflowNodeConfig = Record<string, string | boolean | number | KVType[] | string[] | undefined>;
|
|
|
|
export type WorkflowNode = {
|
|
id: string;
|
|
name: string;
|
|
type: WorkflowNodeType;
|
|
validated?: boolean;
|
|
|
|
input?: WorkflowNodeIo[];
|
|
config?: WorkflowNodeConfig;
|
|
output?: WorkflowNodeIo[];
|
|
|
|
next?: WorkflowNode | WorkflowBranchNode;
|
|
};
|
|
|
|
type NewWorkflowNodeOptions = {
|
|
branchIndex?: number;
|
|
providerType?: string;
|
|
};
|
|
|
|
export const initWorkflow = (): Workflow => {
|
|
// 开始节点
|
|
const rs = newWorkflowNode(WorkflowNodeType.Start, {});
|
|
let root = rs;
|
|
|
|
// 申请节点
|
|
root.next = newWorkflowNode(WorkflowNodeType.Apply, {});
|
|
root = root.next;
|
|
|
|
// 部署节点
|
|
root.next = newWorkflowNode(WorkflowNodeType.Deploy, {});
|
|
root = root.next;
|
|
|
|
// 通知节点
|
|
root.next = newWorkflowNode(WorkflowNodeType.Notify, {});
|
|
|
|
return {
|
|
id: "",
|
|
name: i18n.t("workflow.default.name"),
|
|
type: "auto",
|
|
crontab: "0 0 * * *",
|
|
enabled: false,
|
|
draft: rs,
|
|
};
|
|
};
|
|
|
|
export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => {
|
|
const id = nanoid();
|
|
const typeName = workflowNodeTypeDefaultName.get(type) || "";
|
|
const name = options.branchIndex !== undefined ? `${typeName} ${options.branchIndex + 1}` : typeName;
|
|
|
|
let rs: WorkflowNode | WorkflowBranchNode = {
|
|
id,
|
|
name,
|
|
type,
|
|
};
|
|
|
|
if (type === WorkflowNodeType.Apply || type === WorkflowNodeType.Deploy) {
|
|
rs = {
|
|
...rs,
|
|
config: {
|
|
providerType: options.providerType,
|
|
},
|
|
input: workflowNodeTypeDefaultInput.get(type),
|
|
output: workflowNodeTypeDefaultOutput.get(type),
|
|
};
|
|
}
|
|
|
|
if (type == WorkflowNodeType.Condition) {
|
|
rs.validated = true;
|
|
}
|
|
|
|
if (type === WorkflowNodeType.Branch) {
|
|
rs = {
|
|
...rs,
|
|
branches: [newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 1 })],
|
|
};
|
|
}
|
|
|
|
return rs;
|
|
};
|
|
|
|
export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => {
|
|
return node.type === WorkflowNodeType.Branch;
|
|
};
|
|
|
|
export const updateNode = (node: WorkflowNode | WorkflowBranchNode, targetNode: WorkflowNode | WorkflowBranchNode) => {
|
|
return produce(node, (draft) => {
|
|
let current = draft;
|
|
while (current) {
|
|
if (current.id === targetNode.id) {
|
|
Object.assign(current, targetNode);
|
|
break;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
current.branches = current.branches.map((branch) => updateNode(branch, targetNode));
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
}
|
|
return draft;
|
|
});
|
|
};
|
|
|
|
export const addNode = (node: WorkflowNode | WorkflowBranchNode, preId: string, targetNode: WorkflowNode | WorkflowBranchNode) => {
|
|
return produce(node, (draft) => {
|
|
let current = draft;
|
|
while (current) {
|
|
if (current.id === preId && !isWorkflowBranchNode(targetNode)) {
|
|
targetNode.next = current.next;
|
|
current.next = targetNode;
|
|
break;
|
|
} else if (current.id === preId && isWorkflowBranchNode(targetNode)) {
|
|
targetNode.branches[0].next = current.next;
|
|
current.next = targetNode;
|
|
break;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
current.branches = current.branches.map((branch) => addNode(branch, preId, targetNode));
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
}
|
|
return draft;
|
|
});
|
|
};
|
|
|
|
export const addBranch = (node: WorkflowNode | WorkflowBranchNode, branchNodeId: string) => {
|
|
return produce(node, (draft) => {
|
|
let current = draft;
|
|
while (current) {
|
|
if (current.id === branchNodeId) {
|
|
if (!isWorkflowBranchNode(current)) {
|
|
return draft;
|
|
}
|
|
current.branches.push(
|
|
newWorkflowNode(WorkflowNodeType.Condition, {
|
|
branchIndex: current.branches.length,
|
|
})
|
|
);
|
|
break;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
current.branches = current.branches.map((branch) => addBranch(branch, branchNodeId));
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
}
|
|
return draft;
|
|
});
|
|
};
|
|
|
|
export const removeNode = (node: WorkflowNode | WorkflowBranchNode, targetNodeId: string) => {
|
|
return produce(node, (draft) => {
|
|
let current = draft;
|
|
while (current) {
|
|
if (current.next?.id === targetNodeId) {
|
|
current.next = current.next.next;
|
|
break;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId));
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
}
|
|
return draft;
|
|
});
|
|
};
|
|
|
|
export const removeBranch = (node: WorkflowNode | WorkflowBranchNode, branchNodeId: string, branchIndex: number) => {
|
|
return produce(node, (draft) => {
|
|
let current = draft;
|
|
let last: WorkflowNode | WorkflowBranchNode | undefined = {
|
|
id: "",
|
|
name: "",
|
|
type: WorkflowNodeType.Start,
|
|
next: draft,
|
|
};
|
|
while (current && last) {
|
|
if (current.id === branchNodeId) {
|
|
if (!isWorkflowBranchNode(current)) {
|
|
return draft;
|
|
}
|
|
current.branches.splice(branchIndex, 1);
|
|
|
|
// 如果仅剩一个分支,删除分支节点,将分支节点的下一个节点挂载到当前节点
|
|
if (current.branches.length === 1) {
|
|
const branch = current.branches[0];
|
|
if (branch.next) {
|
|
last.next = branch.next;
|
|
let lastNode: WorkflowNode | WorkflowBranchNode | undefined = branch.next;
|
|
while (lastNode?.next) {
|
|
lastNode = lastNode.next;
|
|
}
|
|
lastNode.next = current.next;
|
|
} else {
|
|
last.next = current.next;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
current.branches = current.branches.map((branch) => removeBranch(branch, branchNodeId, branchIndex));
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
last = last.next;
|
|
}
|
|
return draft;
|
|
});
|
|
};
|
|
|
|
// 1 个分支的节点,不应该能获取到相邻分支上节点的输出
|
|
export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNode, id: string, type: string): WorkflowNode[] => {
|
|
const output: WorkflowNode[] = [];
|
|
|
|
const traverse = (current: WorkflowNode | WorkflowBranchNode, output: WorkflowNode[]) => {
|
|
if (!current) {
|
|
return false;
|
|
}
|
|
if (current.id === id) {
|
|
return true;
|
|
}
|
|
|
|
if (!isWorkflowBranchNode(current) && current.output && current.output.some((io) => io.type === type)) {
|
|
output.push({
|
|
...current,
|
|
output: current.output.filter((io) => io.type === type),
|
|
});
|
|
}
|
|
|
|
if (isWorkflowBranchNode(current)) {
|
|
const currentLength = output.length;
|
|
console.log(currentLength);
|
|
for (const branch of current.branches) {
|
|
if (traverse(branch, output)) {
|
|
return true;
|
|
}
|
|
// 如果当前分支没有输出,清空之前的输出
|
|
if (output.length > currentLength) {
|
|
output.splice(currentLength);
|
|
}
|
|
}
|
|
}
|
|
|
|
return traverse(current.next as WorkflowNode, output);
|
|
};
|
|
|
|
traverse(node, output);
|
|
return output;
|
|
};
|
|
|
|
export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => {
|
|
let current = node;
|
|
while (current) {
|
|
if (!isWorkflowBranchNode(current) && !current.validated) {
|
|
return false;
|
|
}
|
|
if (isWorkflowBranchNode(current)) {
|
|
for (const branch of current.branches) {
|
|
if (!allNodesValidated(branch)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
current = current.next as WorkflowNode;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: string } => {
|
|
if (node.type === WorkflowNodeType.Start) {
|
|
return {
|
|
type: (node.config?.executionMethod as string) ?? "",
|
|
crontab: (node.config?.crontab as string) ?? "",
|
|
};
|
|
} else {
|
|
return {
|
|
type: "",
|
|
crontab: "",
|
|
};
|
|
}
|
|
};
|
|
|
|
export type WorkflowBranchNode = {
|
|
id: string;
|
|
name: string;
|
|
type: WorkflowNodeType;
|
|
branches: WorkflowNode[];
|
|
next?: WorkflowNode | WorkflowBranchNode;
|
|
};
|
|
|
|
type WorkflowwNodeDropdwonItem = {
|
|
type: WorkflowNodeType;
|
|
providerType?: string;
|
|
name: string;
|
|
icon: WorkflowwNodeDropdwonItemIcon;
|
|
leaf?: boolean;
|
|
children?: WorkflowwNodeDropdwonItem[];
|
|
};
|
|
|
|
export enum WorkflowwNodeDropdwonItemIconType {
|
|
Icon,
|
|
Provider,
|
|
}
|
|
|
|
export type WorkflowwNodeDropdwonItemIcon = {
|
|
type: WorkflowwNodeDropdwonItemIconType;
|
|
name: string;
|
|
};
|
|
|
|
const workflowNodeDropdownDeployList: WorkflowwNodeDropdwonItem[] = deployTargets.map((item) => {
|
|
return {
|
|
type: WorkflowNodeType.Apply,
|
|
providerType: item.type,
|
|
name: i18n.t(item.name),
|
|
leaf: true,
|
|
icon: {
|
|
type: WorkflowwNodeDropdwonItemIconType.Provider,
|
|
name: item.icon,
|
|
},
|
|
};
|
|
});
|
|
|
|
export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
|
|
{
|
|
type: WorkflowNodeType.Apply,
|
|
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Apply) ?? "",
|
|
icon: {
|
|
type: WorkflowwNodeDropdwonItemIconType.Icon,
|
|
name: "NotebookPen",
|
|
},
|
|
leaf: true,
|
|
},
|
|
{
|
|
type: WorkflowNodeType.Deploy,
|
|
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Deploy) ?? "",
|
|
icon: {
|
|
type: WorkflowwNodeDropdwonItemIconType.Icon,
|
|
name: "CloudUpload",
|
|
},
|
|
children: workflowNodeDropdownDeployList,
|
|
},
|
|
{
|
|
type: WorkflowNodeType.Branch,
|
|
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Branch) ?? "",
|
|
leaf: true,
|
|
icon: {
|
|
type: WorkflowwNodeDropdwonItemIconType.Icon,
|
|
name: "GitFork",
|
|
},
|
|
},
|
|
{
|
|
type: WorkflowNodeType.Notify,
|
|
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Notify) ?? "",
|
|
leaf: true,
|
|
icon: {
|
|
type: WorkflowwNodeDropdwonItemIconType.Icon,
|
|
name: "Megaphone",
|
|
},
|
|
},
|
|
];
|
|
|