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"`
Name string `json:"name"`
Config map[string]any `json:"config"`
Config map[string]any `json:"data"`
Inputs []WorkflowNodeIO `json:"inputs"`
Outputs []WorkflowNodeIO `json:"outputs"`

View File

@ -42,6 +42,7 @@
"workflow.detail.baseinfo.form.description.label": "Description",
"workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description",
"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.confirm": "Are you sure to discard your changes?",
"workflow.detail.orchestration.action.release": "Release",

View File

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

View File

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

View File

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