0e7cb9e5cf
* 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
269 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|