feat(plugin-workflow): add cancel action for execution (#3425)
* feat(plugin-workflow): add cancel action for execution * feat(plugin-workflow): add cancel button in execution canvas * fix(plugin-workflow-manual): fix canceled execution actions
This commit is contained in:
parent
dc61f10f78
commit
fb11c82b79
@ -31,7 +31,7 @@ import WorkflowPlugin, {
|
|||||||
import { DetailsBlockProvider } from './instruction/DetailsBlockProvider';
|
import { DetailsBlockProvider } from './instruction/DetailsBlockProvider';
|
||||||
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
||||||
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE, useLang } from '../locale';
|
||||||
|
|
||||||
const nodeCollection = {
|
const nodeCollection = {
|
||||||
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
||||||
@ -211,6 +211,15 @@ const UserColumn = observer(
|
|||||||
{ displayName: 'UserColumn' },
|
{ displayName: 'UserColumn' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function UserJobStatusColumn(props) {
|
||||||
|
const record = useRecord();
|
||||||
|
const labelUnprocessed = useLang('Unprocessed');
|
||||||
|
if (record.execution.status && !record.status) {
|
||||||
|
return <Tag>{labelUnprocessed}</Tag>;
|
||||||
|
}
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
|
||||||
export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC } = () => {
|
export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC } = () => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
@ -218,6 +227,7 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC }
|
|||||||
NodeColumn,
|
NodeColumn,
|
||||||
WorkflowColumn,
|
WorkflowColumn,
|
||||||
UserColumn,
|
UserColumn,
|
||||||
|
UserJobStatusColumn,
|
||||||
}}
|
}}
|
||||||
schema={{
|
schema={{
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -326,6 +336,8 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC }
|
|||||||
title: `{{t("Status", { ns: "workflow" })}}`,
|
title: `{{t("Status", { ns: "workflow" })}}`,
|
||||||
properties: {
|
properties: {
|
||||||
status: {
|
status: {
|
||||||
|
type: 'number',
|
||||||
|
'x-decorator': 'UserJobStatusColumn',
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
@ -400,16 +412,16 @@ function ActionBarProvider(props) {
|
|||||||
const ManualActionStatusContext = createContext<number | null>(null);
|
const ManualActionStatusContext = createContext<number | null>(null);
|
||||||
|
|
||||||
function ManualActionStatusProvider({ value, children }) {
|
function ManualActionStatusProvider({ value, children }) {
|
||||||
const { userJob } = useFlowContext();
|
const { userJob, execution } = useFlowContext();
|
||||||
const button = useField();
|
const button = useField();
|
||||||
const buttonSchema = useFieldSchema();
|
const buttonSchema = useFieldSchema();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userJob.status) {
|
if (execution.status || userJob.status) {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.visible = userJob.status === value && userJob.result._ === buttonSchema.name;
|
button.visible = userJob.status === value && userJob.result._ === buttonSchema.name;
|
||||||
}
|
}
|
||||||
}, [userJob, value, button, buttonSchema.name]);
|
}, [execution, userJob, value, button, buttonSchema.name]);
|
||||||
|
|
||||||
return <ManualActionStatusContext.Provider value={value}>{children}</ManualActionStatusContext.Provider>;
|
return <ManualActionStatusContext.Provider value={value}>{children}</ManualActionStatusContext.Provider>;
|
||||||
}
|
}
|
||||||
@ -420,12 +432,12 @@ function useSubmit() {
|
|||||||
const { values, submit } = useForm();
|
const { values, submit } = useForm();
|
||||||
const buttonSchema = useFieldSchema();
|
const buttonSchema = useFieldSchema();
|
||||||
const { service } = useTableBlockContext();
|
const { service } = useTableBlockContext();
|
||||||
const { userJob } = useFlowContext();
|
const { userJob, execution } = useFlowContext();
|
||||||
const { name: actionKey } = buttonSchema;
|
const { name: actionKey } = buttonSchema;
|
||||||
const { name: formKey } = buttonSchema.parent.parent;
|
const { name: formKey } = buttonSchema.parent.parent;
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
if (userJob.status) {
|
if (execution.status || userJob.status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await submit();
|
await submit();
|
||||||
@ -456,7 +468,7 @@ function FlowContextProvider(props) {
|
|||||||
.resource('users_jobs')
|
.resource('users_jobs')
|
||||||
.get?.({
|
.get?.({
|
||||||
filterByTk: id,
|
filterByTk: id,
|
||||||
appends: ['node', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const { node, workflow: { nodes = [], ...workflow } = {}, execution, ...userJob } = data?.data ?? {};
|
const { node, workflow: { nodes = [], ...workflow } = {}, execution, ...userJob } = data?.data ?? {};
|
||||||
@ -517,18 +529,19 @@ function FlowContextProvider(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useFormBlockProps() {
|
function useFormBlockProps() {
|
||||||
const { userJob } = useFlowContext();
|
const { userJob, execution } = useFlowContext();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
const { data: user } = useCurrentUserContext();
|
const { data: user } = useCurrentUserContext();
|
||||||
const { form } = useFormBlockContext();
|
const { form } = useFormBlockContext();
|
||||||
|
|
||||||
const pattern = userJob.status
|
const pattern =
|
||||||
? record
|
execution.status || userJob.status
|
||||||
? 'readPretty'
|
? record
|
||||||
: 'disabled'
|
? 'readPretty'
|
||||||
: user?.data?.id !== userJob.userId
|
: 'disabled'
|
||||||
? 'disabled'
|
: user?.data?.id !== userJob.userId
|
||||||
: 'editable';
|
? 'disabled'
|
||||||
|
: 'editable';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form?.setPattern(pattern);
|
form?.setPattern(pattern);
|
||||||
@ -613,7 +626,7 @@ function Decorator({ params = {}, children }) {
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
sort: ['-createdAt'],
|
sort: ['-createdAt'],
|
||||||
...params,
|
...params,
|
||||||
appends: ['user', 'node', 'workflow'],
|
appends: ['user', 'node', 'workflow', 'execution.status'],
|
||||||
except: ['node.config', 'workflow.config', 'workflow.options'],
|
except: ['node.config', 'workflow.config', 'workflow.options'],
|
||||||
},
|
},
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
|
@ -26,5 +26,6 @@
|
|||||||
"Update record form": "更新数据表单",
|
"Update record form": "更新数据表单",
|
||||||
"Filter settings": "筛选设置",
|
"Filter settings": "筛选设置",
|
||||||
"Workflow todos": "工作流待办",
|
"Workflow todos": "工作流待办",
|
||||||
"Task node": "任务节点"
|
"Task node": "任务节点",
|
||||||
|
"Unprocessed": "未处理"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Breadcrumb, Dropdown, Result, Space, Spin, Tag } from 'antd';
|
import { Breadcrumb, Button, Dropdown, message, Modal, Result, Space, Spin, Tag, Tooltip } from 'antd';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionContextProvider,
|
ActionContextProvider,
|
||||||
@ -22,9 +22,10 @@ import { FlowContext, useFlowContext } from './FlowContext';
|
|||||||
import { lang, NAMESPACE } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
import { linkNodes } from './utils';
|
import { linkNodes } from './utils';
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
import { DownOutlined, ExclamationCircleFilled, StopOutlined } from '@ant-design/icons';
|
||||||
import { StatusButton } from './components/StatusButton';
|
import { StatusButton } from './components/StatusButton';
|
||||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function attachJobs(nodes, jobs: any[] = []): void {
|
function attachJobs(nodes, jobs: any[] = []): void {
|
||||||
const nodesMap = new Map();
|
const nodesMap = new Map();
|
||||||
@ -210,16 +211,40 @@ function ExecutionsDropdown(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExecutionCanvas() {
|
export function ExecutionCanvas() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { data, loading } = useResourceActionContext();
|
const { data, loading, refresh } = useResourceActionContext();
|
||||||
const { setTitle } = useDocumentTitle();
|
const { setTitle } = useDocumentTitle();
|
||||||
const [viewJob, setViewJob] = useState(null);
|
const [viewJob, setViewJob] = useState(null);
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
const apiClient = useAPIClient();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { workflow } = data?.data ?? {};
|
const { workflow } = data?.data ?? {};
|
||||||
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
|
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
|
||||||
}, [data?.data]);
|
}, [data?.data]);
|
||||||
|
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: lang('Cancel the execution'),
|
||||||
|
icon: <ExclamationCircleFilled />,
|
||||||
|
content: lang('Are you sure you want to cancel the execution?'),
|
||||||
|
onOk: () => {
|
||||||
|
apiClient
|
||||||
|
.resource('executions')
|
||||||
|
.cancel({
|
||||||
|
filterByTk: data?.data.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success(t('Operation succeeded'));
|
||||||
|
refresh();
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
console.error(response.data.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [data?.data]);
|
||||||
|
|
||||||
if (!data?.data) {
|
if (!data?.data) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spin />;
|
return <Spin />;
|
||||||
@ -258,6 +283,11 @@ export function ExecutionCanvas() {
|
|||||||
</header>
|
</header>
|
||||||
<aside>
|
<aside>
|
||||||
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
||||||
|
{execution.status ? null : (
|
||||||
|
<Tooltip title={lang('Cancel the execution')}>
|
||||||
|
<Button type="link" danger onClick={onCancel} shape="circle" size="small" icon={<StopOutlined />} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
<time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,7 @@ import { executionSchema } from './schemas/executions';
|
|||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
import { linkNodes } from './utils';
|
import { linkNodes } from './utils';
|
||||||
import { getWorkflowDetailPath } from './constant';
|
import { getWorkflowDetailPath } from './constant';
|
||||||
|
import { ExecutionStatusColumn } from './components/ExecutionStatus';
|
||||||
|
|
||||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
@ -230,6 +231,7 @@ export function WorkflowCanvas() {
|
|||||||
components={{
|
components={{
|
||||||
ExecutionResourceProvider,
|
ExecutionResourceProvider,
|
||||||
ExecutionLink,
|
ExecutionLink,
|
||||||
|
ExecutionStatusColumn,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
|
@ -6,7 +6,7 @@ import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
|||||||
import { WorkflowLink } from './WorkflowLink';
|
import { WorkflowLink } from './WorkflowLink';
|
||||||
import OpenDrawer from './components/OpenDrawer';
|
import OpenDrawer from './components/OpenDrawer';
|
||||||
import { workflowSchema } from './schemas/workflows';
|
import { workflowSchema } from './schemas/workflows';
|
||||||
import { ExecutionStatusSelect } from './components/ExecutionStatusSelect';
|
import { ExecutionStatusSelect, ExecutionStatusColumn } from './components/ExecutionStatus';
|
||||||
import WorkflowPlugin from '.';
|
import WorkflowPlugin from '.';
|
||||||
|
|
||||||
export function WorkflowPane() {
|
export function WorkflowPane() {
|
||||||
@ -23,6 +23,7 @@ export function WorkflowPane() {
|
|||||||
ExecutionLink,
|
ExecutionLink,
|
||||||
OpenDrawer,
|
OpenDrawer,
|
||||||
ExecutionStatusSelect,
|
ExecutionStatusSelect,
|
||||||
|
ExecutionStatusColumn,
|
||||||
}}
|
}}
|
||||||
scope={{
|
scope={{
|
||||||
getTriggersOptions,
|
getTriggersOptions,
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Button, Modal, Select, Tag, Tooltip, message } from 'antd';
|
||||||
|
import { ExclamationCircleFilled, StopOutlined } from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Action, css, useCompile, useRecord, useResourceActionContext, useResourceContext } from '@nocobase/client';
|
||||||
|
|
||||||
|
import { EXECUTION_STATUS, ExecutionStatusOptions, ExecutionStatusOptionsMap } from '../constants';
|
||||||
|
import { lang } from '../locale';
|
||||||
|
|
||||||
|
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 }) {
|
||||||
|
const mode = props.multiple ? 'multiple' : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
// @ts-ignore
|
||||||
|
role="button"
|
||||||
|
data-testid={`select-${mode || 'single'}`}
|
||||||
|
{...props}
|
||||||
|
mode={mode}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExecutionStatusColumn(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
const { resource } = useResourceContext();
|
||||||
|
const record = useRecord();
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: lang('Cancel the execution'),
|
||||||
|
icon: <ExclamationCircleFilled />,
|
||||||
|
content: lang('Are you sure you want to cancel the execution?'),
|
||||||
|
onOk: () => {
|
||||||
|
resource
|
||||||
|
.cancel({
|
||||||
|
filterByTk: record.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success(t('Operation succeeded'));
|
||||||
|
refresh();
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
console.error(response.data.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [record]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
{record.status ? null : (
|
||||||
|
<Tooltip title={lang('Cancel the execution')}>
|
||||||
|
<Button type="link" danger onClick={onCancel} shape="circle" size="small" icon={<StopOutlined />} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
import { Select, Tag } from 'antd';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
|
|
||||||
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 }) {
|
|
||||||
const mode = props.multiple ? 'multiple' : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
// @ts-ignore
|
|
||||||
role="button"
|
|
||||||
data-testid={`select-${mode || 'single'}`}
|
|
||||||
{...props}
|
|
||||||
mode={mode}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -11,10 +11,21 @@ import { getWorkflowDetailPath } from '../constant';
|
|||||||
export const executionCollection = {
|
export const executionCollection = {
|
||||||
name: 'executions',
|
name: 'executions',
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
interface: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
name: 'id',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
} as ISchema,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
interface: 'createdAt',
|
interface: 'createdAt',
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
// field: 'createdAt',
|
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
@ -120,6 +131,18 @@ export const executionSchema = {
|
|||||||
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
|
'x-component': 'Table.Column',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'Table.Column.Decorator',
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
@ -151,9 +174,11 @@ export const executionSchema = {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'Table.Column.Decorator',
|
'x-decorator': 'Table.Column.Decorator',
|
||||||
'x-component': 'Table.Column',
|
'x-component': 'Table.Column',
|
||||||
|
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
|
||||||
properties: {
|
properties: {
|
||||||
status: {
|
status: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
'x-decorator': 'ExecutionStatusColumn',
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
@ -173,7 +198,6 @@ export const executionSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
link: {
|
link: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t("Details", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-component': 'ExecutionLink',
|
'x-component': 'ExecutionLink',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,9 @@
|
|||||||
"Rejected from a manual node.": "被人工节点拒绝继续。",
|
"Rejected from a manual node.": "被人工节点拒绝继续。",
|
||||||
"General failed but should do another try.": "执行失败,需重试。",
|
"General failed but should do another try.": "执行失败,需重试。",
|
||||||
|
|
||||||
|
"Cancel the execution": "取消执行",
|
||||||
|
"Are you sure you want to cancel the execution?": "确定要取消该执行吗?",
|
||||||
|
|
||||||
"Operations": "操作",
|
"Operations": "操作",
|
||||||
"This node contains branches, deleting will also be preformed to them, are you sure?":
|
"This node contains branches, deleting will also be preformed to them, are you sure?":
|
||||||
"节点包含分支,将同时删除其所有分支下的子节点,确定继续?",
|
"节点包含分支,将同时删除其所有分支下的子节点,确定继续?",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import actions, { Context } from '@nocobase/actions';
|
import actions, { Context } from '@nocobase/actions';
|
||||||
import { Op } from '@nocobase/database';
|
import { Op } from '@nocobase/database';
|
||||||
import { EXECUTION_STATUS } from '../constants';
|
import { EXECUTION_STATUS, JOB_STATUS } from '../constants';
|
||||||
|
|
||||||
export async function destroy(context: Context, next) {
|
export async function destroy(context: Context, next) {
|
||||||
context.action.mergeParams({
|
context.action.mergeParams({
|
||||||
@ -13,3 +13,43 @@ export async function destroy(context: Context, next) {
|
|||||||
|
|
||||||
await actions.destroy(context, next);
|
await actions.destroy(context, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancel(context: Context, next) {
|
||||||
|
const { filterByTk } = context.action.params;
|
||||||
|
const ExecutionRepo = context.db.getRepository('executions');
|
||||||
|
const JobRepo = context.db.getRepository('jobs');
|
||||||
|
const execution = await ExecutionRepo.findOne({
|
||||||
|
filterByTk,
|
||||||
|
appends: ['jobs'],
|
||||||
|
});
|
||||||
|
if (!execution) {
|
||||||
|
return context.throw(404);
|
||||||
|
}
|
||||||
|
if (execution.status) {
|
||||||
|
return context.throw(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.db.sequelize.transaction(async (transaction) => {
|
||||||
|
await execution.update(
|
||||||
|
{
|
||||||
|
status: EXECUTION_STATUS.CANCELED,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
|
||||||
|
const pendingJobs = execution.jobs.filter((job) => job.status === JOB_STATUS.PENDING);
|
||||||
|
await JobRepo.update({
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.CANCELED,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: pendingJobs.map((job) => job.id),
|
||||||
|
},
|
||||||
|
individualHooks: false,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context.body = execution;
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user