feat(flowEditor): 实现应用运行状态隔离与运行日志管理

- 在 ideContainer 状态中增加 appRuntimeData 字段,用于按应用 ID 隔离存储运行状态
- 修改 FlowEditorMain 和 actionBar 组件,使用当前应用的独立运行状态控制界面交互
- 更新节点拖拽、连接、删除等操作的禁用逻辑,基于当前应用运行状态判断
- 在 logBar 中实现运行日志的分应用存储与展示功能
- 添加 addRuntimeLog 和 clearRuntimeLogs actions 用于管理各应用的运行日志- 优化 useFlowEditorState 和 useFlowCallbacks 钩子以支持新的状态结构
- 确保在应用启动时清空对应应用的历史运行日志
master
钟良源 3 months ago
parent 3232a2e08f
commit 2e26666c03

@ -15,7 +15,7 @@ import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import { updateCanvasDataMap, resetNodeStatus, updateIsRunning, updateEventListOld } from '@/store/ideContainer';
import { updateCanvasDataMap, resetNodeStatus, updateIsRunning, updateEventListOld, addRuntimeLog, clearRuntimeLogs } from '@/store/ideContainer';
import {
validateAllNodes,
showValidationErrors,
@ -1301,6 +1301,9 @@ export const useFlowCallbacks = (
animationProgress: 0
}
})));
// 清空当前应用的运行日志
dispatch(clearRuntimeLogs({ appId: currentAppData.id }));
}
}, [initialData?.appId]);
@ -1340,4 +1343,4 @@ export const useFlowCallbacks = (
handleRun
};
};
export default useFlowCallbacks;
export default useFlowCallbacks;

@ -9,9 +9,14 @@ import { Dispatch } from 'redux';
export const useFlowEditorState = (initialData?: any) => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const { canvasDataMap, nodeStatusMap, isRunning } = useSelector((state: any) => state.ideContainer);
const { canvasDataMap, nodeStatusMap, isRunning, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
const dispatch = useDispatch();
// 获取当前应用的运行状态
const currentAppIsRunning = currentAppData?.id && appRuntimeData[currentAppData.id]
? appRuntimeData[currentAppData.id].isRunning
: false;
// 添加编辑弹窗相关状态
const [editingNode, setEditingNode] = useState<Node | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
@ -33,11 +38,11 @@ export const useFlowEditorState = (initialData?: any) => {
data: {
...node.data,
status: nodeStatusMap[node.id] || 'waiting',
isStatusVisible: isRunning // 只有在运行时才显示状态指示器
isStatusVisible: currentAppIsRunning // 只有在运行时才显示状态指示器
}
}))
);
}, [nodeStatusMap, isRunning]);
}, [nodeStatusMap, currentAppIsRunning]);
const updateCanvasDataMapDebounced = useRef(
debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => {
@ -65,7 +70,7 @@ export const useFlowEditorState = (initialData?: any) => {
setEdgeForNodeAdd,
positionForNodeAdd,
setPositionForNodeAdd,
isRunning,
isRunning: currentAppIsRunning, // 使用当前应用的运行状态
historyInitialized,
setHistoryInitialized,
historyTimeoutRef,

@ -24,6 +24,7 @@ import ActionBar from './components/actionBar';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import { useHistory } from './components/historyContext';
import { NodeTypes } from '@xyflow/react';
import { useSelector } from 'react-redux';
const edgeTypes = {
custom: CustomEdge
@ -137,6 +138,12 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
const { undo, redo, canUndo, canRedo } = useHistory();
const reactFlowId = useMemo(() => new Date().getTime().toString(), []);
// 从Redux store中获取当前应用的运行状态
const { appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
const currentAppIsRunning = currentAppData?.id && appRuntimeData[currentAppData.id]
? appRuntimeData[currentAppData.id].isRunning
: false;
// 监听键盘事件实现快捷键
useEffect(() => {
@ -178,17 +185,17 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
onContextMenu={(e) => e.preventDefault()}>
<ReactFlow
id={reactFlowId}
nodes={nodes.map(node => ({ ...node, draggable: !isRunning }))} // 运行时禁用节点拖拽
nodes={nodes.map(node => ({ ...node, draggable: !currentAppIsRunning }))} // 运行时禁用节点拖拽
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid={true}
snapGrid={[2, 2]}
nodesConnectable={!isRunning} // 运行时禁用节点连接
nodesDraggable={!isRunning} // 运行时禁用节点拖拽
elementsSelectable={!isRunning} // 运行时禁用元素选择
connectOnClick={!isRunning} // 运行时禁用点击连接
disableKeyboardA11y={isRunning} // 运行时禁用键盘交互
nodesConnectable={!currentAppIsRunning} // 运行时禁用节点连接
nodesDraggable={!currentAppIsRunning} // 运行时禁用节点拖拽
elementsSelectable={!currentAppIsRunning} // 运行时禁用元素选择
connectOnClick={!currentAppIsRunning} // 运行时禁用点击连接
disableKeyboardA11y={currentAppIsRunning} // 运行时禁用键盘交互
onBeforeDelete={async ({ nodes }) => {
// 检查是否有开始或结束节点
const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end');
@ -203,7 +210,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
);
// 允许删除操作继续进行
return !isRunning; // 运行时禁止删除节点
return !currentAppIsRunning; // 运行时禁止删除节点
}}
onNodesDelete={(deleted) => {
// 检查是否有循环节点
@ -262,21 +269,21 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
setIsEditModalOpen(false);
}}
onNodesChange={!isRunning ? onNodesChange : undefined} // 运行时禁用节点变更
onEdgesChange={!isRunning ? onEdgesChange : undefined} // 运行时禁用边变更
onConnect={!isRunning ? onConnect : undefined} // 运行时禁用连接
onReconnect={!isRunning ? onReconnect : undefined} // 运行时禁用重新连接
onDragOver={!isRunning ? onDragOver : undefined} // 运行时禁用拖拽
onDrop={!isRunning ? onDrop : undefined} // 运行时禁用放置
onNodeDrag={!isRunning ? onNodeDrag : undefined} // 运行时禁用节点拖拽
onNodesChange={!currentAppIsRunning ? onNodesChange : undefined} // 运行时禁用节点变更
onEdgesChange={!currentAppIsRunning ? onEdgesChange : undefined} // 运行时禁用边变更
onConnect={!currentAppIsRunning ? onConnect : undefined} // 运行时禁用连接
onReconnect={!currentAppIsRunning ? onReconnect : undefined} // 运行时禁用重新连接
onDragOver={!currentAppIsRunning ? onDragOver : undefined} // 运行时禁用拖拽
onDrop={!currentAppIsRunning ? onDrop : undefined} // 运行时禁用放置
onNodeDrag={!currentAppIsRunning ? onNodeDrag : undefined} // 运行时禁用节点拖拽
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={!isRunning ? onNodeDragStop : undefined} // 运行时禁用节点拖拽停止
onNodeContextMenu={!isRunning ? onNodeContextMenu : undefined} // 运行时禁用节点上下文菜单
onNodeDoubleClick={!isRunning ? onNodeDoubleClick : undefined} // 运行时禁用节点双击
onEdgeContextMenu={!isRunning ? onEdgeContextMenu : undefined} // 运行时禁用边上下文菜单
onPaneClick={!isRunning ? onPaneClick : undefined} // 运行时禁用面板点击
onPaneContextMenu={!isRunning ? onPaneContextMenu : undefined} // 运行时禁用面板上下文菜单
onNodeDragStop={!currentAppIsRunning ? onNodeDragStop : undefined} // 运行时禁用节点拖拽停止
onNodeContextMenu={!currentAppIsRunning ? onNodeContextMenu : undefined} // 运行时禁用节点上下文菜单
onNodeDoubleClick={!currentAppIsRunning ? onNodeDoubleClick : undefined} // 运行时禁用节点双击
onEdgeContextMenu={!currentAppIsRunning ? onEdgeContextMenu : undefined} // 运行时禁用边上下文菜单
onPaneClick={!currentAppIsRunning ? onPaneClick : undefined} // 运行时禁用面板点击
onPaneContextMenu={!currentAppIsRunning ? onPaneContextMenu : undefined} // 运行时禁用面板上下文菜单
onEdgeMouseEnter={(_event, edge) => {
setEdges((eds) => eds.map(e => {
if (e.id === edge.id) {
@ -294,8 +301,8 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}));
}}
fitView
selectionOnDrag={!isRunning} // 运行时禁用拖拽选择
selectionMode={!isRunning ? SelectionMode.Partial : undefined} // 运行时禁用选择模式
selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择
selectionMode={!currentAppIsRunning ? SelectionMode.Partial : undefined} // 运行时禁用选择模式
>
<Background />
<Panel position="top-left">
@ -307,14 +314,14 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
isRunning={isRunning}
isRunning={currentAppIsRunning}
></ActionBar>
</Panel>
<AlignmentGuides />
</ReactFlow>
{/*节点右键上下文*/}
{!isRunning && menu && menu.type === 'node' && (
{!currentAppIsRunning && menu && menu.type === 'node' && (
<div
style={{
position: 'absolute',
@ -335,7 +342,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
)}
{/*边右键上下文*/}
{!isRunning && menu && menu.type === 'edge' && (
{!currentAppIsRunning && menu && menu.type === 'edge' && (
<div
style={{
position: 'absolute',
@ -358,7 +365,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
)}
{/*画布右键上下文*/}
{!isRunning && menu && menu.type === 'pane' && (
{!currentAppIsRunning && menu && menu.type === 'pane' && (
<div
style={{
position: 'absolute',
@ -382,14 +389,14 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
<NodeEditModal
popupContainer={reactFlowWrapper}
node={editingNode}
isOpen={isEditModalOpen && !isRunning}
isOpen={isEditModalOpen && !currentAppIsRunning}
isDelete={isDelete}
onSave={saveNodeEdit}
onClose={closeEditModal}
/>
{/*统一的添加节点菜单*/}
{!isRunning && (edgeForNodeAdd || positionForNodeAdd) && (
{!currentAppIsRunning && (edgeForNodeAdd || positionForNodeAdd) && (
<div
style={{
position: 'absolute',

@ -27,15 +27,20 @@ const ActionBar: React.FC<ActionBarProps> = ({
onRun,
isRunning = false
}) => {
const { logBarStatus } = useSelector((state) => state.ideContainer);
const { logBarStatus, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
const dispatch = useDispatch();
// 获取当前应用的运行状态
const currentAppIsRunning = currentAppData?.id && appRuntimeData[currentAppData.id]
? appRuntimeData[currentAppData.id].isRunning
: false;
const changeLogBarStatus = () => {
dispatch(updateLogBarStatus(!logBarStatus));
};
const handleRun = () => {
onRun?.(!isRunning);
onRun?.(!currentAppIsRunning);
};
return (
@ -50,9 +55,9 @@ const ActionBar: React.FC<ActionBarProps> = ({
icon={<IconPlayArrow />}
onClick={() => handleRun()}
style={{ padding: '0 8px', backgroundColor: '#fff' }}
status={isRunning ? 'danger' : undefined}
status={currentAppIsRunning ? 'danger' : undefined}
>
{isRunning ? '停止' : '运行'}
{currentAppIsRunning ? '停止' : '运行'}
</Button>
<Button
type="outline"

@ -44,11 +44,12 @@ const LogBar: React.FC<LogBarProps> = () => {
const [tabs] = useState(data);
const [activeTab, setActiveTab] = useState('1');
const resizeBoxRef = useRef<HTMLDivElement>(null); // 引用 ResizeBox 容器
const { logBarStatus } = useSelector((state) => state.ideContainer);
const { logBarStatus, appRuntimeData } = useSelector((state: any) => state.ideContainer);
const dispatch = useDispatch();
const [validationLogs, setValidationLogs] = useState<LogMessage[]>([]);
const [runtimeLogs, setRuntimeLogs] = useState<LogMessage[]>([]); // 添加运行时日志状态
const [logContainerHeight, setLogContainerHeight] = useState('250px'); // 添加日志容器高度状态
const { currentAppData } = useSelector((state: any) => state.ideContainer);
// 处理 Tab 点击事件
const handleTabClick = (key: string) => {
@ -120,6 +121,18 @@ const LogBar: React.FC<LogBarProps> = () => {
// 自动切换到运行日志tab并展开logBar
setActiveTab('1');
dispatch(updateLogBarStatus(true));
// 同时将日志添加到当前应用的运行日志中
const appId = currentAppData?.id;
if (appId) {
dispatch({
type: 'ideContainer/addRuntimeLog',
payload: {
log: newLog,
appId: appId
}
});
}
}
};
@ -130,7 +143,7 @@ const LogBar: React.FC<LogBarProps> = () => {
return () => {
document.removeEventListener('logMessage', handleLogMessage as EventListener);
};
}, [dispatch]);
}, [dispatch, currentAppData?.id]);
// 渲染校验日志内容
const renderValidationLogs = () => {
@ -156,12 +169,17 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染运行时日志内容
const renderRuntimeLogs = () => {
// 获取当前应用的运行日志
const currentAppLogs = currentAppData?.id && appRuntimeData[currentAppData.id]
? appRuntimeData[currentAppData.id].logs || []
: [];
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
{runtimeLogs.length === 0 ? (
{currentAppLogs.length === 0 ? (
<p></p>
) : (
runtimeLogs.map(log => (
currentAppLogs.map((log: LogMessage) => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()}

@ -15,6 +15,12 @@ interface IDEContainerState {
socketId: string;
nodeStatusMap: Record<string, string>; // 节点状态映射
isRunning: boolean; // 是否正在运行
// 应用运行状态和日志数据按应用ID隔离存储
appRuntimeData: Record<string, {
nodeStatusMap: Record<string, string>;
isRunning: boolean;
logs: any[];
}>;
}
// 初始状态
@ -31,7 +37,8 @@ const initialState: IDEContainerState = {
logBarStatus: false,
socketId: '', // 工程的socketId
nodeStatusMap: {}, // 初始化节点状态映射
isRunning: false // 默认未运行
isRunning: false, // 默认未运行
appRuntimeData: {}, // 按应用ID隔离的应用运行状态和日志数据
};
// 创建切片
@ -82,14 +89,65 @@ const ideContainerSlice = createSlice({
// 对于运行时更新,我们仍然更新状态但标记为运行时状态
state.nodeStatusMap[nodeId] = status;
}
// 同时更新当前应用的节点状态
const appId = state.currentAppData?.id;
if (appId) {
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = {
nodeStatusMap: {},
isRunning: false,
logs: []
};
}
state.appRuntimeData[appId].nodeStatusMap[nodeId] = status;
}
},
// 重置节点状态
resetNodeStatus: (state) => {
state.nodeStatusMap = {};
// 同时重置当前应用的节点状态
const appId = state.currentAppData?.id;
if (appId && state.appRuntimeData[appId]) {
state.appRuntimeData[appId].nodeStatusMap = {};
}
},
// 更新运行状态
updateIsRunning: (state, { payload }) => {
state.isRunning = payload;
// 同时更新当前应用的运行状态
const appId = state.currentAppData?.id;
if (appId) {
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = {
nodeStatusMap: {},
isRunning: false,
logs: []
};
}
state.appRuntimeData[appId].isRunning = payload;
}
},
// 添加运行日志
addRuntimeLog: (state, { payload }) => {
const { log, appId } = payload;
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = {
nodeStatusMap: {},
isRunning: false,
logs: []
};
}
state.appRuntimeData[appId].logs.push(log);
},
// 清空指定应用的运行日志
clearRuntimeLogs: (state, { payload }) => {
const { appId } = payload;
if (state.appRuntimeData[appId]) {
state.appRuntimeData[appId].logs = [];
}
}
}
});
@ -109,7 +167,9 @@ export const {
updateSocketId,
updateNodeStatus,
resetNodeStatus,
updateIsRunning
updateIsRunning,
addRuntimeLog,
clearRuntimeLogs
} = ideContainerSlice.actions;
// 默认导出 reducer

Loading…
Cancel
Save