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