流程定义
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,
|
||||
createBy: "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 = [
|
||||
{
|
||||
title: '编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
@ -160,16 +159,16 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 200,
|
||||
title: '标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
dataIndex: 'flowVersion',
|
||||
key: 'flowVersion',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
@ -188,13 +187,11 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 100,
|
||||
render: (enabled: boolean) => (
|
||||
<Tag color={enabled ? 'success' : 'default'}>{enabled ? '已启用' : '已禁用'}</Tag>
|
||||
),
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
@ -202,6 +199,24 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'createBy',
|
||||
key: 'createBy',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '更新人',
|
||||
dataIndex: 'updateBy',
|
||||
key: 'updateBy',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
@ -341,7 +356,7 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
label="名称"
|
||||
rules={[{required: true, message: '请输入流程名称'}]}
|
||||
>
|
||||
<Input placeholder="请输入流程名称"/>
|
||||
<Input placeholder="请输入流<EFBFBD><EFBFBD><EFBFBD>名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea placeholder="请输入流程描述"/>
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
NodeTypeQuery
|
||||
} 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';
|
||||
|
||||
// 创建工作流定义
|
||||
|
||||
@ -37,6 +37,7 @@ const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/E
|
||||
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
|
||||
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
|
||||
const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer'));
|
||||
const LogStreamPage = lazy(() => import('../pages/LogStream'));
|
||||
|
||||
// 创建路由
|
||||
const router = createBrowserRouter([
|
||||
@ -164,6 +165,14 @@ const router = createBrowserRouter([
|
||||
<WorkflowMonitor/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'log-stream/:processInstanceId',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<LogStreamPage/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user