mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:36:44 +00:00
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
This commit is contained in:
parent
9881d69def
commit
fa43d9c870
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
@ -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<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
const { color } = ExecutionStatusOptionsMap[props.value] ?? {};
|
||||
return (
|
||||
<Tag color={color} onMouseDown={onPreventMouseDown} closable={props.closable} onClose={props.onClose}>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutionStatusOption(props) {
|
||||
const compile = useCompile();
|
||||
return (
|
||||
<>
|
||||
<LabelTag {...props} />
|
||||
{props.description ? <span>{compile(props.description)}</span> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExecutionStatusSelect({ ...props }) {
|
||||
return (
|
||||
<Select {...props} mode={props.multiple ? 'multiple' : null} optionLabelProp="label" tagRender={LabelTag}>
|
||||
{ExecutionStatusOptions.filter((item) => Boolean(item.value) && item.value !== EXECUTION_STATUS.ABORTED).map(
|
||||
(option) => (
|
||||
<Select.Option key={option.value} {...option}>
|
||||
<ExecutionStatusOption {...option} />
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
@ -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(
|
||||
|
@ -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 }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -65,7 +65,7 @@ const useStyles = createStyles(({ css, token }) => {
|
||||
text-align: right;
|
||||
|
||||
time {
|
||||
width: 14em;
|
||||
width: 12em;
|
||||
color: ${token.colorText};
|
||||
font-size: 80%;
|
||||
}
|
||||
|
@ -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': '暂存',
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -31,7 +31,10 @@ export default class Processor {
|
||||
jobsMap = new Map<number, JobModel>();
|
||||
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);
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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')) {
|
||||
|
@ -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: [
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user