feat: release and discard workflow changes

This commit is contained in:
Fu Diwei 2025-01-05 02:38:01 +08:00
parent 9c4831fa3f
commit 7cf96d7d7e
6 changed files with 206 additions and 165 deletions

View File

@ -45,7 +45,7 @@ type WorkflowNode struct {
Type WorkflowNodeType `json:"type"` Type WorkflowNodeType `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Config map[string]any `json:"config"` Config map[string]any `json:"data"`
Inputs []WorkflowNodeIO `json:"inputs"` Inputs []WorkflowNodeIO `json:"inputs"`
Outputs []WorkflowNodeIO `json:"outputs"` Outputs []WorkflowNodeIO `json:"outputs"`

View File

@ -42,6 +42,7 @@
"workflow.detail.baseinfo.form.description.label": "Description", "workflow.detail.baseinfo.form.description.label": "Description",
"workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description", "workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description",
"workflow.detail.orchestration.tab": "Orchestration", "workflow.detail.orchestration.tab": "Orchestration",
"workflow.detail.orchestration.draft.alert": "The orchestration is not released yet.",
"workflow.detail.orchestration.action.discard": "Discard changes", "workflow.detail.orchestration.action.discard": "Discard changes",
"workflow.detail.orchestration.action.discard.confirm": "Are you sure to discard your changes?", "workflow.detail.orchestration.action.discard.confirm": "Are you sure to discard your changes?",
"workflow.detail.orchestration.action.release": "Release", "workflow.detail.orchestration.action.release": "Release",

View File

@ -42,6 +42,7 @@
"workflow.detail.baseinfo.form.description.label": "描述", "workflow.detail.baseinfo.form.description.label": "描述",
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述", "workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
"workflow.detail.orchestration.tab": "流程编排", "workflow.detail.orchestration.tab": "流程编排",
"workflow.detail.orchestration.draft.alert": "当前编排有未发布的更改。",
"workflow.detail.orchestration.action.discard": "撤销更改", "workflow.detail.orchestration.action.discard": "撤销更改",
"workflow.detail.orchestration.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?", "workflow.detail.orchestration.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
"workflow.detail.orchestration.action.release": "发布更改", "workflow.detail.orchestration.action.release": "发布更改",

View File

@ -12,7 +12,7 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useDeepCompareEffect } from "ahooks"; import { useDeepCompareEffect } from "ahooks";
import { Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } from "antd"; import { Alert, Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { isEqual } from "radash"; import { isEqual } from "radash";
@ -39,15 +39,15 @@ const WorkflowDetail = () => {
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { id: workflowId } = useParams(); const { id: workflowId } = useParams();
const { workflow, initialized, init, save, destroy, setBaseInfo, switchEnable } = useWorkflowStore( const { workflow, initialized, ...workflowState } = useWorkflowStore(
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "save", "setBaseInfo", "switchEnable"]) useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setBaseInfo", "setEnabled", "release", "discard"])
); );
useEffect(() => { useEffect(() => {
// TODO: loading & error // TODO: loading & error
init(workflowId!); workflowState.init(workflowId!);
return () => { return () => {
destroy(); workflowState.destroy();
}; };
}, [workflowId]); }, [workflowId]);
@ -68,7 +68,7 @@ const WorkflowDetail = () => {
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => { const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
try { try {
await setBaseInfo(values.name!, values.description!); await workflowState.setBaseInfo(values.name!, values.description!);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
@ -83,7 +83,7 @@ const WorkflowDetail = () => {
} }
try { try {
await switchEnable(); await workflowState.setEnabled(!workflow.enabled);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
@ -112,8 +112,15 @@ const WorkflowDetail = () => {
modalApi.confirm({ modalApi.confirm({
title: t("workflow.detail.orchestration.action.discard"), title: t("workflow.detail.orchestration.action.discard"),
content: t("workflow.detail.orchestration.action.discard.confirm"), content: t("workflow.detail.orchestration.action.discard.confirm"),
onOk: () => { onOk: async () => {
alert("TODO"); try {
await workflowState.discard();
messageApi.success(t("common.text.operation_succeeded"));
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
}, },
}); });
}; };
@ -129,7 +136,7 @@ const WorkflowDetail = () => {
content: t("workflow.detail.orchestration.action.release.confirm"), content: t("workflow.detail.orchestration.action.release.confirm"),
onOk: async () => { onOk: async () => {
try { try {
await save(); await workflowState.release();
messageApi.success(t("common.text.operation_succeeded")); messageApi.success(t("common.text.operation_succeeded"));
} catch (err) { } catch (err) {
@ -242,38 +249,45 @@ const WorkflowDetail = () => {
<Card loading={!initialized}> <Card loading={!initialized}>
<Show when={tabValue === "orchestration"}> <Show when={tabValue === "orchestration"}>
<div className="relative"> <div className="relative">
<div className="py-12 lg:pr-36 xl:pr-48"> <div className="flex items-center justify-between gap-4">
<WorkflowElements /> <div className="flex-1 overflow-hidden">
</div> <Show when={workflow.hasDraft!}>
<div className="absolute right-0 top-0 z-[1]"> <Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
<Space> </Show>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}> </div>
{t("workflow.detail.orchestration.action.run")} <div className="flex justify-end">
</Button> <Space>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
<Button.Group> {t("workflow.detail.orchestration.action.run")}
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
{t("workflow.detail.orchestration.action.release")}
</Button> </Button>
<Dropdown <Button.Group>
menu={{ <Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
items: [ {t("workflow.detail.orchestration.action.release")}
{ </Button>
key: "discard",
disabled: !allowDiscard, <Dropdown
label: t("workflow.detail.orchestration.action.discard"), menu={{
icon: <UndoOutlinedIcon />, items: [
onClick: handleDiscardClick, {
}, key: "discard",
], disabled: !allowDiscard,
}} label: t("workflow.detail.orchestration.action.discard"),
trigger={["click"]} icon: <UndoOutlinedIcon />,
> onClick: handleDiscardClick,
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" /> },
</Dropdown> ],
</Button.Group> }}
</Space> trigger={["click"]}
>
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
</Dropdown>
</Button.Group>
</Space>
</div>
</div>
<div className="px-12 py-8">
<WorkflowElements />
</div> </div>
</div> </div>
</Show> </Show>

View File

@ -35,7 +35,7 @@ export const get = async (id: string) => {
}); });
}; };
export const save = async (record: Record<string, string | boolean | WorkflowNode>) => { export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
if (record.id) { if (record.id) {
return await getPocketBase() return await getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME)

View File

@ -1,11 +1,12 @@
import { produce } from "immer";
import { create } from "zustand"; import { create } from "zustand";
import { import {
type WorkflowModel, type WorkflowModel,
type WorkflowNode, type WorkflowNode,
type WorkflowNodeConfigForStart,
addBranch, addBranch,
addNode, addNode,
getExecuteMethod,
getWorkflowOutputBeforeId, getWorkflowOutputBeforeId,
removeBranch, removeBranch,
removeNode, removeNode,
@ -16,17 +17,22 @@ import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow"
export type WorkflowState = { export type WorkflowState = {
workflow: WorkflowModel; workflow: WorkflowModel;
initialized: boolean; initialized: boolean;
updateNode: (node: WorkflowNode) => void;
addNode: (node: WorkflowNode, preId: string) => void;
addBranch: (branchId: string) => void;
removeNode: (nodeId: string) => void;
removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
switchEnable(): void;
save(): void;
setBaseInfo: (name: string, description: string) => void;
init(id: string): void; init(id: string): void;
setBaseInfo: (name: string, description: string) => void;
setEnabled(enabled: boolean): void;
release(): void;
discard(): void;
destroy(): void; destroy(): void;
addNode: (node: WorkflowNode, preId: string) => void;
updateNode: (node: WorkflowNode) => void;
removeNode: (nodeId: string) => void;
addBranch: (branchId: string) => void;
removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
}; };
export const useWorkflowStore = create<WorkflowState>((set, get) => ({ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
@ -42,171 +48,197 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}); });
}, },
destroy: () => {
set({
workflow: {} as WorkflowModel,
initialized: false,
});
},
setBaseInfo: async (name: string, description: string) => { setBaseInfo: async (name: string, description: string) => {
const data: Record<string, string | boolean | WorkflowNode> = { if (!get().initialized) throw "Workflow not initialized yet";
id: (get().workflow.id as string) ?? "",
const resp = await saveWorkflow({
id: get().workflow.id!,
name: name || "", name: name || "",
description: description || "", description: description || "",
};
const resp = await saveWorkflow(data);
set((state: WorkflowState) => {
return {
workflow: {
...state.workflow,
name,
description,
id: resp.id,
},
};
});
},
switchEnable: async () => {
const root = get().workflow.content as WorkflowNode;
const executeMethod = getExecuteMethod(root);
const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "",
content: root,
enabled: !get().workflow.enabled,
trigger: executeMethod.trigger,
triggerCron: executeMethod.triggerCron,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.name = resp.name;
id: resp.id, draft.description = resp.description;
content: resp.content, }),
enabled: resp.enabled,
trigger: resp.trigger,
triggerCron: resp.triggerCron,
},
}; };
}); });
}, },
save: async () => { setEnabled: async (enabled: boolean) => {
const root = get().workflow.draft as WorkflowNode; if (!get().initialized) throw "Workflow not initialized yet";
const executeMethod = getExecuteMethod(root);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: get().workflow.id!,
enabled: enabled,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.enabled = resp.enabled;
}),
};
});
},
release: async () => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = get().workflow.draft!;
const startConfig = root.config as WorkflowNodeConfigForStart;
const resp = await saveWorkflow({
id: get().workflow.id!,
trigger: startConfig.trigger,
triggerCron: startConfig.triggerCron,
content: root, content: root,
hasDraft: false, hasDraft: false,
trigger: executeMethod.trigger,
triggerCron: executeMethod.triggerCron,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.trigger = resp.trigger;
id: resp.id, draft.triggerCron = resp.triggerCron;
content: resp.content, draft.content = resp.content;
hasDraft: false, draft.draft = resp.draft;
trigger: resp.trigger, draft.hasDraft = resp.hasDraft;
triggerCron: resp.triggerCron, }),
},
}; };
}); });
}, },
updateNode: async (node: WorkflowNode) => { discard: async () => {
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node); if (!get().initialized) throw "Workflow not initialized yet";
const root = get().workflow.content!;
const startConfig = root.config as WorkflowNodeConfigForStart;
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: get().workflow.id!,
draft: newRoot, draft: root,
hasDraft: true, hasDraft: false,
trigger: startConfig.trigger,
triggerCron: startConfig.triggerCron,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.trigger = resp.trigger;
draft: newRoot, draft.triggerCron = resp.triggerCron;
id: resp.id, draft.content = resp.content;
hasDraft: true, draft.draft = resp.draft;
}, draft.hasDraft = resp.hasDraft;
}),
}; };
}); });
}, },
addNode: async (node: WorkflowNode, preId: string) => { addNode: async (node: WorkflowNode, preId: string) => {
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node); if (!get().initialized) throw "Workflow not initialized yet";
const root = addNode(get().workflow.draft!, preId, node);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: get().workflow.id!,
draft: newRoot, draft: root,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.draft = resp.draft;
draft: newRoot, draft.hasDraft = resp.hasDraft;
id: resp.id, }),
hasDraft: true,
},
}; };
}); });
}, },
addBranch: async (branchId: string) => { updateNode: async (node: WorkflowNode) => {
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId); if (!get().initialized) throw "Workflow not initialized yet";
const root = updateNode(get().workflow.draft!, node);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: get().workflow.id!,
draft: newRoot, draft: root,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.draft = resp.draft;
draft: newRoot, draft.hasDraft = resp.hasDraft;
id: resp.id, }),
hasDraft: true,
},
};
});
},
removeBranch: async (branchId: string, index: number) => {
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "",
draft: newRoot,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: {
...state.workflow,
draft: newRoot,
id: resp.id,
hasDraft: true,
},
}; };
}); });
}, },
removeNode: async (nodeId: string) => { removeNode: async (nodeId: string) => {
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId); if (!get().initialized) throw "Workflow not initialized yet";
const root = removeNode(get().workflow.draft!, nodeId);
const resp = await saveWorkflow({ const resp = await saveWorkflow({
id: (get().workflow.id as string) ?? "", id: get().workflow.id!,
draft: newRoot, draft: root,
hasDraft: true, hasDraft: true,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
workflow: { workflow: produce(state.workflow, (draft) => {
...state.workflow, draft.draft = resp.draft;
draft: newRoot, draft.hasDraft = resp.hasDraft;
id: resp.id, }),
hasDraft: true, };
}, });
},
addBranch: async (branchId: string) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = addBranch(get().workflow.draft!, branchId);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
};
});
},
removeBranch: async (branchId: string, index: number) => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = removeBranch(get().workflow.draft!, branchId, index);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
hasDraft: true,
});
set((state: WorkflowState) => {
return {
workflow: produce(state.workflow, (draft) => {
draft.draft = resp.draft;
draft.hasDraft = resp.hasDraft;
}),
}; };
}); });
}, },
@ -214,11 +246,4 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
getWorkflowOuptutBeforeId: (id: string, type: string) => { getWorkflowOuptutBeforeId: (id: string, type: string) => {
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
}, },
destroy: () => {
set({
workflow: {} as WorkflowModel,
initialized: false,
});
},
})); }));