From fa43d9c8705875ef7885fb9e564a64708daeacc9 Mon Sep 17 00:00:00 2001 From: Junyi Date: Thu, 10 Aug 2023 15:18:07 +0700 Subject: [PATCH] feat(plugin-workflow): allow to configure auto delete execution in history (#2423) * feat(plugin-workflow): allow to configure auto delete execution * fix(plugin-workflow): fix locale --- .../src/collection-manager/action-hooks.ts | 4 +- .../workflow/src/client/WorkflowProvider.tsx | 2 + .../components/ExecutionStatusSelect.tsx | 44 +++++++++ .../plugins/workflow/src/client/constants.tsx | 56 +++++++++-- .../workflow/src/client/schemas/workflows.ts | 92 +++++++++++-------- .../plugins/workflow/src/client/style.tsx | 2 +- packages/plugins/workflow/src/locale/zh-CN.ts | 12 +++ .../plugins/workflow/src/server/Plugin.ts | 4 +- .../plugins/workflow/src/server/Processor.ts | 19 ++-- .../src/server/__tests__/Plugin.test.ts | 92 +++++++++++++++++++ .../workflow/src/server/actions/workflows.ts | 2 +- .../src/server/collections/workflows.ts | 5 + .../20230809113132-workflow-options.ts | 32 +++++++ 13 files changed, 309 insertions(+), 57 deletions(-) create mode 100644 packages/plugins/workflow/src/client/components/ExecutionStatusSelect.tsx create mode 100644 packages/plugins/workflow/src/server/migrations/20230809113132-workflow-options.ts diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 5dd35f86ef..e58e1c8643 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -342,7 +342,9 @@ export const useCreateAction = () => { field.data.loading = false; refresh(); } catch (error) { - field.data.loading=false; + if (field.data) { + field.data.loading = false; + } } }, }; diff --git a/packages/plugins/workflow/src/client/WorkflowProvider.tsx b/packages/plugins/workflow/src/client/WorkflowProvider.tsx index 008c27212d..23d7a5f1da 100644 --- a/packages/plugins/workflow/src/client/WorkflowProvider.tsx +++ b/packages/plugins/workflow/src/client/WorkflowProvider.tsx @@ -15,6 +15,7 @@ import { lang } from './locale'; import { instructions } from './nodes'; import { workflowSchema } from './schemas/workflows'; import { triggers } from './triggers'; +import { ExecutionStatusSelect } from './components/ExecutionStatusSelect'; // registerField(expressionField.group, 'expression', expressionField); @@ -35,6 +36,7 @@ function WorkflowPane() { ExecutionResourceProvider, ExecutionLink, OpenDrawer, + ExecutionStatusSelect, }} /> diff --git a/packages/plugins/workflow/src/client/components/ExecutionStatusSelect.tsx b/packages/plugins/workflow/src/client/components/ExecutionStatusSelect.tsx new file mode 100644 index 0000000000..0234ce9bd8 --- /dev/null +++ b/packages/plugins/workflow/src/client/components/ExecutionStatusSelect.tsx @@ -0,0 +1,44 @@ +import React, { useCallback } from 'react'; +import { Select, Tag } from 'antd'; + +import { useCompile } from '@nocobase/client'; +import { EXECUTION_STATUS, ExecutionStatusOptions, ExecutionStatusOptionsMap } from '../constants'; + +function LabelTag(props) { + const compile = useCompile(); + const label = compile(props.label); + const onPreventMouseDown = useCallback((event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }, []); + const { color } = ExecutionStatusOptionsMap[props.value] ?? {}; + return ( + + {label} + + ); +} + +function ExecutionStatusOption(props) { + const compile = useCompile(); + return ( + <> + + {props.description ? {compile(props.description)} : null} + + ); +} + +export function ExecutionStatusSelect({ ...props }) { + return ( + + ); +} diff --git a/packages/plugins/workflow/src/client/constants.tsx b/packages/plugins/workflow/src/client/constants.tsx index bdad7184ef..3d7ad17e5c 100644 --- a/packages/plugins/workflow/src/client/constants.tsx +++ b/packages/plugins/workflow/src/client/constants.tsx @@ -20,14 +20,54 @@ export const EXECUTION_STATUS = { }; export const ExecutionStatusOptions = [ - { value: EXECUTION_STATUS.QUEUEING, label: `{{t("Queueing", { ns: "${NAMESPACE}" })}}`, color: 'blue' }, - { value: EXECUTION_STATUS.STARTED, label: `{{t("On going", { ns: "${NAMESPACE}" })}}`, color: 'gold' }, - { value: EXECUTION_STATUS.RESOLVED, label: `{{t("Resolved", { ns: "${NAMESPACE}" })}}`, color: 'green' }, - { value: EXECUTION_STATUS.FAILED, label: `{{t("Failed", { ns: "${NAMESPACE}" })}}`, color: 'red' }, - { value: EXECUTION_STATUS.ERROR, label: `{{t("Error", { ns: "${NAMESPACE}" })}}`, color: 'red' }, - { value: EXECUTION_STATUS.ABORTED, label: `{{t("Aborted", { ns: "${NAMESPACE}" })}}`, color: 'red' }, - { value: EXECUTION_STATUS.CANCELED, label: `{{t("Canceled", { ns: "${NAMESPACE}" })}}`, color: 'volcano' }, - { value: EXECUTION_STATUS.REJECTED, label: `{{t("Rejected", { ns: "${NAMESPACE}" })}}`, color: 'volcano' }, + { + value: EXECUTION_STATUS.QUEUEING, + label: `{{t("Queueing", { ns: "${NAMESPACE}" })}}`, + color: 'blue', + description: `{{t("Triggered but still waiting in queue to execute.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.STARTED, + label: `{{t("On going", { ns: "${NAMESPACE}" })}}`, + color: 'gold', + description: `{{t("Started and executing, maybe waiting for an async callback (manual, delay etc.).", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.RESOLVED, + label: `{{t("Resolved", { ns: "${NAMESPACE}" })}}`, + color: 'green', + description: `{{t("Successfully finished.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.FAILED, + label: `{{t("Failed", { ns: "${NAMESPACE}" })}}`, + color: 'red', + description: `{{t("Failed to satisfy node configurations.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.ERROR, + label: `{{t("Error", { ns: "${NAMESPACE}" })}}`, + color: 'red', + description: `{{t("Some node meets error.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.ABORTED, + label: `{{t("Aborted", { ns: "${NAMESPACE}" })}}`, + color: 'red', + description: `{{t("Running of some node was aborted by program flow.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.CANCELED, + label: `{{t("Canceled", { ns: "${NAMESPACE}" })}}`, + color: 'volcano', + description: `{{t("Manually canceled whole execution when waiting.", { ns: "${NAMESPACE}" })}}`, + }, + { + value: EXECUTION_STATUS.REJECTED, + label: `{{t("Rejected", { ns: "${NAMESPACE}" })}}`, + color: 'volcano', + description: `{{t("Rejected from a manual node.", { ns: "${NAMESPACE}" })}}`, + }, ]; export const ExecutionStatusOptionsMap = ExecutionStatusOptions.reduce( diff --git a/packages/plugins/workflow/src/client/schemas/workflows.ts b/packages/plugins/workflow/src/client/schemas/workflows.ts index e50896f88b..1a13297ae3 100644 --- a/packages/plugins/workflow/src/client/schemas/workflows.ts +++ b/packages/plugins/workflow/src/client/schemas/workflows.ts @@ -74,9 +74,49 @@ const collection = { 'x-decorator': 'FormItem', } as ISchema, }, + { + type: 'object', + name: 'options', + }, ], }; +const workflowFieldset = { + title: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + type: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + description: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + options: { + type: 'object', + 'x-component': 'fieldset', + properties: { + useTransaction: { + type: 'boolean', + title: `{{ t("Use transaction", { ns: "${NAMESPACE}" }) }}`, + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, + deleteExecutionOnStatus: { + type: 'array', + title: `{{ t("Auto delete history when execution is on end status", { ns: "${NAMESPACE}" }) }}`, + 'x-decorator': 'FormItem', + 'x-component': 'ExecutionStatusSelect', + 'x-component-props': { + multiple: true, + }, + }, + }, + }, +}; + export const workflowSchema: ISchema = { type: 'void', properties: { @@ -132,18 +172,7 @@ export const workflowSchema: ISchema = { }, title: '{{t("Add new")}}', properties: { - title: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, - type: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, - description: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, + ...workflowFieldset, footer: { type: 'void', 'x-component': 'Action.Drawer.Footer', @@ -283,7 +312,7 @@ export const workflowSchema: ISchema = { split: '|', }, properties: { - config: { + view: { type: 'void', 'x-component': 'WorkflowLink', }, @@ -304,18 +333,7 @@ export const workflowSchema: ISchema = { }, title: '{{ t("Edit") }}', properties: { - title: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, - enabled: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, - description: { - 'x-component': 'CollectionField', - 'x-decorator': 'FormItem', - }, + ...workflowFieldset, footer: { type: 'void', 'x-component': 'Action.Drawer.Footer', @@ -403,18 +421,18 @@ export const workflowSchema: ISchema = { }, }, }, - // delete: { - // type: 'void', - // title: '{{ t("Delete") }}', - // 'x-component': 'Action.Link', - // 'x-component-props': { - // confirm: { - // title: "{{t('Delete record')}}", - // content: "{{t('Are you sure you want to delete it?')}}", - // }, - // useAction: '{{ cm.useDestroyActionAndRefreshCM }}', - // }, - // }, + delete: { + type: 'void', + title: '{{ t("Delete") }}', + 'x-component': 'Action.Link', + 'x-component-props': { + confirm: { + title: "{{t('Delete record')}}", + content: "{{t('Are you sure you want to delete it?')}}", + }, + useAction: '{{ cm.useDestroyActionAndRefreshCM }}', + }, + }, }, }, }, diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index eeee355a0c..e754ebea16 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -65,7 +65,7 @@ const useStyles = createStyles(({ css, token }) => { text-align: right; time { - width: 14em; + width: 12em; color: ${token.colorText}; font-size: 80%; } diff --git a/packages/plugins/workflow/src/locale/zh-CN.ts b/packages/plugins/workflow/src/locale/zh-CN.ts index f4c6b0484f..13a0cf6a86 100644 --- a/packages/plugins/workflow/src/locale/zh-CN.ts +++ b/packages/plugins/workflow/src/locale/zh-CN.ts @@ -18,6 +18,8 @@ export default { 'Delete a main version will cause all other revisions to be deleted too.': '删除主版本将导致其他版本一并被删除。', Loading: '加载中', 'Load failed': '加载失败', + 'Use transaction': '启用事务', + 'Auto delete history when execution is on end status': '执行结束后自动删除对应状态的历史记录', Trigger: '触发器', 'Trigger variables': '触发器变量', 'Trigger data': '触发数据', @@ -103,6 +105,16 @@ export default { Canceled: '已取消', Rejected: '已拒绝', + 'Triggered but still waiting in queue to execute.': '已触发但仍在队列中等待执行。', + 'Started and executing, maybe waiting for an async callback (manual, delay etc.).': + '已开始执行,可能在等待异步回调(人工、延时等)。', + 'Successfully finished.': '成功完成。', + 'Failed to satisfy node configurations.': '未满足节点配置造成的失败。', + 'Some node meets error.': '某个节点出错。', + 'Running of some node was aborted by program flow.': '某个节点被程序流程终止。', + 'Manually canceled whole execution when waiting.': '等待时被手动取消整个执行。', + 'Rejected from a manual node.': '被人工节点拒绝继续。', + 'Continue the process': '继续流程', 'Terminate the process': '终止流程', 'Save temporarily': '暂存', diff --git a/packages/plugins/workflow/src/server/Plugin.ts b/packages/plugins/workflow/src/server/Plugin.ts index 41a11fb60b..21c7246f95 100644 --- a/packages/plugins/workflow/src/server/Plugin.ts +++ b/packages/plugins/workflow/src/server/Plugin.ts @@ -253,7 +253,6 @@ export default class WorkflowPlugin extends Plugin { context, key: workflow.key, status: EXECUTION_STATUS.QUEUEING, - useTransaction: workflow.useTransaction, }, { transaction }, ); @@ -350,6 +349,9 @@ export default class WorkflowPlugin extends Plugin { this.getLogger(execution.workflowId).info( `execution (${execution.id}) finished with status: ${execution.status}`, ); + if (execution.status && execution.workflow.options?.deleteExecutionOnStatus?.includes(execution.status)) { + await execution.destroy(); + } } catch (err) { this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err); } diff --git a/packages/plugins/workflow/src/server/Processor.ts b/packages/plugins/workflow/src/server/Processor.ts index 473315d113..8bcf86b732 100644 --- a/packages/plugins/workflow/src/server/Processor.ts +++ b/packages/plugins/workflow/src/server/Processor.ts @@ -31,7 +31,10 @@ export default class Processor { jobsMap = new Map(); jobsMapByNodeId: { [key: number]: any } = {}; - constructor(public execution: ExecutionModel, public options: ProcessorOptions) { + constructor( + public execution: ExecutionModel, + public options: ProcessorOptions, + ) { this.logger = options.plugin.getLogger(execution.workflowId); } @@ -63,7 +66,7 @@ export default class Processor { } private async getTransaction() { - if (!this.execution.useTransaction) { + if (!this.execution.workflow.options?.useTransaction) { return; } @@ -76,15 +79,15 @@ export default class Processor { } public async prepare() { + const { execution } = this; + if (!execution.workflow) { + execution.workflow = await execution.getWorkflow(); + } + const transaction = await this.getTransaction(); this.transaction = transaction; - const { execution } = this; - if (!execution.workflow) { - execution.workflow = await execution.getWorkflow({ transaction }); - } - - const nodes = await execution.workflow.getNodes({ transaction }); + const nodes = await execution.workflow.getNodes(); this.makeNodes(nodes); diff --git a/packages/plugins/workflow/src/server/__tests__/Plugin.test.ts b/packages/plugins/workflow/src/server/__tests__/Plugin.test.ts index 67b7ee2372..024af75093 100644 --- a/packages/plugins/workflow/src/server/__tests__/Plugin.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/Plugin.test.ts @@ -395,4 +395,96 @@ describe('workflow > Plugin', () => { expect(e2.status).toBe(EXECUTION_STATUS.QUEUEING); }); }); + + describe('options.deleteExecutionOnStatus', () => { + it('no configured should not be deleted', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const executions = await w1.getExecutions(); + expect(executions.length).toBe(1); + }); + + it('status on started should not be deleted', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + options: { + deleteExecutionOnStatus: [EXECUTION_STATUS.STARTED], + }, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + + await w1.createNode({ + type: 'pending', + }); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const executions = await w1.getExecutions(); + expect(executions.length).toBe(1); + expect(executions[0].status).toBe(EXECUTION_STATUS.STARTED); + }); + + it('configured resolved status should be deleted', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + options: { + deleteExecutionOnStatus: [EXECUTION_STATUS.RESOLVED], + }, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const executions = await w1.getExecutions(); + expect(executions.length).toBe(0); + }); + + it('configured error status should be deleted', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + options: { + deleteExecutionOnStatus: [EXECUTION_STATUS.ERROR], + }, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + + await w1.createNode({ + type: 'error', + }); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const executions = await w1.getExecutions(); + expect(executions.length).toBe(0); + }); + }); }); diff --git a/packages/plugins/workflow/src/server/actions/workflows.ts b/packages/plugins/workflow/src/server/actions/workflows.ts index b2e6c83137..6ef774577f 100644 --- a/packages/plugins/workflow/src/server/actions/workflows.ts +++ b/packages/plugins/workflow/src/server/actions/workflows.ts @@ -5,7 +5,7 @@ export async function update(context: Context, next) { const repository = utils.getRepositoryFromParams(context) as Repository; const { filterByTk, values } = context.action.params; context.action.mergeParams({ - whitelist: ['title', 'description', 'enabled', 'config'], + whitelist: ['title', 'description', 'enabled', 'config', 'options'], }); // only enable/disable if (Object.keys(values).includes('config')) { diff --git a/packages/plugins/workflow/src/server/collections/workflows.ts b/packages/plugins/workflow/src/server/collections/workflows.ts index a8190eb771..56c02de01c 100644 --- a/packages/plugins/workflow/src/server/collections/workflows.ts +++ b/packages/plugins/workflow/src/server/collections/workflows.ts @@ -76,6 +76,11 @@ export default function () { constraints: false, onDelete: 'NO ACTION', }, + { + type: 'jsonb', + name: 'options', + defaultValue: {}, + }, ], // NOTE: use unique index for avoiding deadlock in mysql when setCurrent indexes: [ diff --git a/packages/plugins/workflow/src/server/migrations/20230809113132-workflow-options.ts b/packages/plugins/workflow/src/server/migrations/20230809113132-workflow-options.ts new file mode 100644 index 0000000000..57fc44f443 --- /dev/null +++ b/packages/plugins/workflow/src/server/migrations/20230809113132-workflow-options.ts @@ -0,0 +1,32 @@ +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + async up() { + const match = await this.app.version.satisfies('<0.11.0-alpha.2'); + if (!match) { + return; + } + const { db } = this.context; + const WorkflowRepo = db.getRepository('flow_nodes'); + await db.sequelize.transaction(async (transaction) => { + const workflows = await WorkflowRepo.find({ + transaction, + }); + + await workflows.reduce( + (promise, workflow) => + promise.then(() => { + workflow.set('options', { + useTransaction: workflow.get('useTransaction'), + }); + workflow.changed('options', true); + return workflow.save({ + silent: true, + transaction, + }); + }), + Promise.resolve(), + ); + }); + } +}