流程定义
This commit is contained in:
parent
559e335d9a
commit
a5793f5367
73
frontend/src/pages/LogStream/components/LogViewer.tsx
Normal file
73
frontend/src/pages/LogStream/components/LogViewer.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { ILogData } from '../types';
|
||||||
|
import { logStreamService } from '../service';
|
||||||
|
|
||||||
|
interface LogViewerProps {
|
||||||
|
processInstanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogViewer: React.FC<LogViewerProps> = ({ processInstanceId }) => {
|
||||||
|
const [logs, setLogs] = useState<ILogData[]>([]);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleLog = (log: ILogData) => {
|
||||||
|
setLogs((prevLogs) => [...prevLogs, log]);
|
||||||
|
// 自动滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error: any) => {
|
||||||
|
setError('日志流连接已断开');
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsConnected(true);
|
||||||
|
logStreamService.connect(processInstanceId, handleLog, handleError);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
logStreamService.disconnect();
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
}, [processInstanceId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-[600px] p-4 border rounded-lg shadow-sm bg-white">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-medium">实时日志</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{isConnected ? '已连接' : '未连接'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="h-[500px] w-full rounded-md border p-4 overflow-auto bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`mb-1 ${log.type === 'STDERR' ? 'text-red-500' : 'text-gray-800'}`}
|
||||||
|
>
|
||||||
|
<span className="text-gray-400">[{new Date(log.timestamp).toLocaleTimeString()}] </span>
|
||||||
|
{log.data}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{error && <div className="text-red-500 mt-2">{error}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogViewer;
|
||||||
19
frontend/src/pages/LogStream/index.tsx
Normal file
19
frontend/src/pages/LogStream/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import LogViewer from './components/LogViewer';
|
||||||
|
|
||||||
|
const LogStreamPage: React.FC = () => {
|
||||||
|
const { processInstanceId } = useParams<{ processInstanceId: string }>();
|
||||||
|
|
||||||
|
if (!processInstanceId) {
|
||||||
|
return <div className="p-4">流程实例ID不能为空</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<LogViewer processInstanceId={processInstanceId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogStreamPage;
|
||||||
54
frontend/src/pages/LogStream/service.ts
Normal file
54
frontend/src/pages/LogStream/service.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ILogData } from './types';
|
||||||
|
|
||||||
|
const LOG_STREAM_URL = '/api/v1/workflow/instance/log';
|
||||||
|
|
||||||
|
export class LogStreamService {
|
||||||
|
private eventSource: EventSource | null = null;
|
||||||
|
|
||||||
|
connect(processInstanceId: string, onMessage: (log: ILogData) => void, onError?: (error: any) => void) {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const url = new URL(`${window.location.origin}${LOG_STREAM_URL}/dc65ecc5-b6f3-11ef-840c-326a31fc0fe1`);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
url.searchParams.append('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource = new EventSource(url.toString());
|
||||||
|
|
||||||
|
this.eventSource.addEventListener('STDOUT', (event) => {
|
||||||
|
onMessage({
|
||||||
|
type: 'STDOUT',
|
||||||
|
data: event.data,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventSource.addEventListener('STDERR', (event) => {
|
||||||
|
onMessage({
|
||||||
|
type: 'STDERR',
|
||||||
|
data: event.data,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventSource.onerror = (error) => {
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
this.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
this.eventSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logStreamService = new LogStreamService();
|
||||||
18
frontend/src/pages/LogStream/types.ts
Normal file
18
frontend/src/pages/LogStream/types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { BaseResponse } from '@/types/base';
|
||||||
|
|
||||||
|
// 日志事件类型
|
||||||
|
export type LogEventType = 'STDOUT' | 'STDERR';
|
||||||
|
|
||||||
|
// 日志数据接口
|
||||||
|
export interface ILogData {
|
||||||
|
type: LogEventType;
|
||||||
|
data: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志流状态
|
||||||
|
export interface ILogStreamState {
|
||||||
|
logs: ILogData[];
|
||||||
|
isConnected: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@ -104,6 +104,23 @@ export const getCurrentUserMenus = async () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
createBy: "system",
|
createBy: "system",
|
||||||
updateBy: "system"
|
updateBy: "system"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: -24,
|
||||||
|
createTime: new Date().toISOString(),
|
||||||
|
updateTime: new Date().toISOString(),
|
||||||
|
version: 0,
|
||||||
|
name: "日志流",
|
||||||
|
path: "/workflow/log-stream/:processInstanceId",
|
||||||
|
component: "/pages/LogStream/index",
|
||||||
|
icon: "file-text",
|
||||||
|
type: MenuTypeEnum.MENU,
|
||||||
|
parentId: -2,
|
||||||
|
sort: 3,
|
||||||
|
hidden: false,
|
||||||
|
enabled: true,
|
||||||
|
createBy: "system",
|
||||||
|
updateBy: "system"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -146,11 +146,10 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
// 表格列定义
|
// 表格列定义
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '编码',
|
title: 'ID',
|
||||||
dataIndex: 'code',
|
dataIndex: 'id',
|
||||||
key: 'code',
|
key: 'id',
|
||||||
width: 150,
|
width: 80,
|
||||||
ellipsis: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
@ -160,16 +159,16 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '标识',
|
||||||
dataIndex: 'description',
|
dataIndex: 'key',
|
||||||
key: 'description',
|
key: 'key',
|
||||||
width: 200,
|
width: 150,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '版本',
|
title: '版本',
|
||||||
dataIndex: 'version',
|
dataIndex: 'flowVersion',
|
||||||
key: 'version',
|
key: 'flowVersion',
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -188,13 +187,11 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '是否启用',
|
title: '描述',
|
||||||
dataIndex: 'enabled',
|
dataIndex: 'description',
|
||||||
key: 'enabled',
|
key: 'description',
|
||||||
width: 100,
|
width: 200,
|
||||||
render: (enabled: boolean) => (
|
ellipsis: true,
|
||||||
<Tag color={enabled ? 'success' : 'default'}>{enabled ? '已启用' : '已禁用'}</Tag>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
@ -202,6 +199,24 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
key: 'createTime',
|
key: 'createTime',
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '创建人',
|
||||||
|
dataIndex: 'createBy',
|
||||||
|
key: 'createBy',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updateTime',
|
||||||
|
key: 'updateTime',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新人',
|
||||||
|
dataIndex: 'updateBy',
|
||||||
|
key: 'updateBy',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
@ -341,7 +356,7 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
label="名称"
|
label="名称"
|
||||||
rules={[{required: true, message: '请输入流程名称'}]}
|
rules={[{required: true, message: '请输入流程名称'}]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入流程名称"/>
|
<Input placeholder="请输入流<EFBFBD><EFBFBD><EFBFBD>名称"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
<Input.TextArea placeholder="请输入流程描述"/>
|
<Input.TextArea placeholder="请输入流程描述"/>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
NodeTypeQuery
|
NodeTypeQuery
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const WORKFLOW_DEFINITION_URL = '/api/v1/workflow-definitions';
|
const WORKFLOW_DEFINITION_URL = '/api/v1/workflow/definition';
|
||||||
const NODE_TYPE_URL = '/api/v1/node-types';
|
const NODE_TYPE_URL = '/api/v1/node-types';
|
||||||
|
|
||||||
// 创建工作流定义
|
// 创建工作流定义
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/E
|
|||||||
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
|
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
|
||||||
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
||||||
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
|
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
|
||||||
|
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||||
|
|
||||||
// 创建路由
|
// 创建路由
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@ -164,6 +165,14 @@ const router = createBrowserRouter([
|
|||||||
<WorkflowMonitor/>
|
<WorkflowMonitor/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'log-stream/:processInstanceId',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<LoadingComponent/>}>
|
||||||
|
<LogStreamPage/>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user