tachybase_todo/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx
Junyi 0e7cb9e5cf
refactor(plugin-workflow): split workflow features into plugins (#3115)
* refactor(plugin-workflow): split manual and dynamic calculation into plugins

* refactor(plugin-workflow): move loop to plugin

* refactor(plugin-workflow): move parallel to plugin

* fix(plugin-dynamic-calculation): fix package title

* fix(plugin-workflow): fix plugin name

* refactor(plugin-workflow): move delay to plugin

* refactor(plugin-workflow): simplify exporting names

* refactor(plugin-workflow): move aggregate to plugin

* refactor(plugin-workflow): move sql to plugin

* refactor(plugin-workflow): move reqeust to plugin

* refactor(plugin-workflow): move form trigger to plugin

* refactor(plugin-workflow): move locale to plugins

* fix(plugin-workflow): fix test cases

* fix(plugin-workflow-request): package name typo

* fix(plugin-workflow): remove clean db from testkit

* fix(plugin-workflow-sql): skip independent case

* fix(plugin-workflow-sql): skip independent case

* fix(plugin-workflow-delay): fix test cases

* test(plugin-workflow-delay): fix test cases

* test(plugin-workflow-delay): fix test cases

* test(plugin-workflow-delay): fix test cases

* test(plugin-workflow-delay): fix test cases

* fix(plugin-workflow): fix migration version matching

* test(plugin-workflow): fix test case

* refactor(plugin-workflow): correct exporting of testkit

* fix(plugin-workflow): fix testkit and require module

* refactor(plugin-workflow): add workflow-test package for testing

* test(plugin-workflow): test weird case

* fix(plugin-workflow-test): remove workflow dependency to avoid cycling

* fix(plugin-workflow): fix migration version

* fix(plugin-workflow): fix migration and packages

* fix(plugin-workflow): fix package dependencies

* fix(preset): fix builtin list in preset

* fix(plugin-workflow): add package entry file

* fix(plugin-workflow): fix migrations

* refactor(plugin-workflow): remove require

* fix(plugin-workflow): fix locale namespace

* fix(plugin-workflow): fix merged errors

* fix(plugin-workflow): fix import cycling references

* refactor(plugin-workflow): change instruction and triggers to classes in client

* fix(plugin-workflow): fix migration version
2023-12-07 05:46:58 -08:00

269 lines
7.6 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Breadcrumb, Dropdown, Result, Space, Spin, Tag } from 'antd';
import {
ActionContextProvider,
cx,
SchemaComponent,
useAPIClient,
useApp,
useCompile,
useDocumentTitle,
usePlugin,
useResourceActionContext,
} from '@nocobase/client';
import { str2moment } from '@nocobase/utils/client';
import WorkflowPlugin from '.';
import { CanvasContent } from './CanvasContent';
import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
import { FlowContext, useFlowContext } from './FlowContext';
import { lang, NAMESPACE } from './locale';
import useStyles from './style';
import { linkNodes } from './utils';
import { DownOutlined } from '@ant-design/icons';
import { StatusButton } from './components/StatusButton';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
function attachJobs(nodes, jobs: any[] = []): void {
const nodesMap = new Map();
nodes.forEach((item) => {
item.jobs = [];
nodesMap.set(item.id, item);
});
jobs.forEach((item) => {
const node = nodesMap.get(item.nodeId);
node.jobs.push(item);
item.node = {
id: node.id,
key: node.key,
title: node.title,
type: node.type,
};
});
nodes.forEach((item) => {
item.jobs = item.jobs.sort((a, b) => a.id - b.id);
});
}
function JobModal() {
const { instructions } = usePlugin(WorkflowPlugin);
const compile = useCompile();
const { viewJob: job, setViewJob } = useFlowContext();
const { styles } = useStyles();
const { node = {} } = job ?? {};
const instruction = instructions.get(node.type);
return (
<ActionContextProvider value={{ visible: Boolean(job), setVisible: setViewJob }}>
<SchemaComponent
schema={{
type: 'void',
properties: {
[`${job?.id}-${job?.updatedAt}-modal`]: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
initialValue: job,
},
'x-component': 'Action.Modal',
title: (
<div className={styles.nodeTitleClass}>
<Tag>{compile(instruction?.title)}</Tag>
<strong>{node.title}</strong>
<span className="workflow-node-id">#{node.id}</span>
</div>
),
properties: {
status: {
type: 'number',
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: JobStatusOptions,
'x-read-pretty': true,
},
updatedAt: {
type: 'string',
title: `{{t("Executed at", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
'x-read-pretty': true,
},
result: {
type: 'object',
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
className: styles.nodeJobResultClass,
},
'x-read-pretty': true,
},
},
},
},
}}
/>
</ActionContextProvider>
);
}
function ExecutionsDropdown(props) {
const { execution } = useFlowContext();
const apiClient = useAPIClient();
const navigate = useNavigate();
const { styles } = useStyles();
const [executionsBefore, setExecutionsBefore] = useState([]);
const [executionsAfter, setExecutionsAfter] = useState([]);
useEffect(() => {
if (!execution) {
return;
}
apiClient
.resource('executions')
.list({
filter: {
key: execution.key,
id: {
$lt: execution.id,
},
},
sort: '-createdAt',
pageSize: 10,
fields: ['id', 'status', 'createdAt'],
})
.then(({ data }) => {
setExecutionsBefore(data.data);
})
.catch(() => {});
}, [execution]);
useEffect(() => {
if (!execution) {
return;
}
apiClient
.resource('executions')
.list({
filter: {
key: execution.key,
id: {
$gt: execution.id,
},
},
sort: 'createdAt',
pageSize: 10,
fields: ['id', 'status', 'createdAt'],
})
.then(({ data }) => {
setExecutionsAfter(data.data.reverse());
})
.catch(() => {});
}, [execution]);
const onClick = useCallback(
({ key }) => {
if (key != execution.id) {
navigate(getWorkflowExecutionsPath(key));
}
},
[execution],
);
return execution ? (
<Dropdown
menu={{
onClick,
defaultSelectedKeys: [`${execution.id}`],
className: cx(styles.dropdownClass, styles.executionsDropdownRowClass),
items: [...executionsAfter, execution, ...executionsBefore].map((item) => {
return {
key: item.id,
label: (
<>
<span className="id">{`#${item.id}`}</span>
<time>{str2moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</>
),
icon: (
<span>
<StatusButton statusMap={ExecutionStatusOptionsMap} status={item.status} />
</span>
),
};
}),
}}
>
<Space>
<strong>{`#${execution.id}`}</strong>
<DownOutlined />
</Space>
</Dropdown>
) : null;
}
export function ExecutionCanvas() {
const compile = useCompile();
const { data, loading } = useResourceActionContext();
const { setTitle } = useDocumentTitle();
const [viewJob, setViewJob] = useState(null);
const app = useApp();
useEffect(() => {
const { workflow } = data?.data ?? {};
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
}, [data?.data]);
if (!data?.data) {
if (loading) {
return <Spin />;
}
return <Result status="404" title="Not found" />;
}
const { jobs = [], workflow: { nodes = [], revisions = [], ...workflow } = {}, ...execution } = data?.data ?? {};
linkNodes(nodes);
attachJobs(nodes, jobs);
const entry = nodes.find((item) => !item.upstream);
const statusOption = ExecutionStatusOptionsMap[execution.status];
return (
<FlowContext.Provider
value={{
workflow: workflow.type ? workflow : null,
nodes,
execution,
viewJob,
setViewJob,
}}
>
<div className="workflow-toolbar">
<header>
<Breadcrumb
items={[
{ title: <Link to={app.pluginSettingsManager.getRoutePath('workflow')}>{lang('Workflow')}</Link> },
{ title: <Link to={getWorkflowDetailPath(workflow.id)}>{workflow.title}</Link> },
{ title: <ExecutionsDropdown /> },
]}
/>
</header>
<aside>
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
<time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</aside>
</div>
<CanvasContent entry={entry} />
<JobModal />
</FlowContext.Provider>
);
}