feat(componentDeployment): 增加组件编译日志功能,优化编辑按钮,优化运行日志和启动按钮渲染逻辑

feature
钟良源 1 month ago
parent eed8b82963
commit a664e6bd90

@ -67,35 +67,44 @@ export const startStatusMap = startStatusDic.reduce((obj, item) => {
/**
*
* BUILT_DOING, BUILT_IGNORE,BUILT_DOCKER,BUILT_FAIL
* BUILT_IDLE
* BUILT_RUNNING
* BUILT_SUCCESS
* BUILT_FAILED
* BUILT_CANCELED
*/
export const compileStatusConstant = {
BUILT_DOING: 'built-doing',
BUILT_IGNORE: 'built-ignore',
BUILT_DOCKER: 'built-docker',
BUILT_FAIL: 'built-fail',
// 前端单独使用
NOT_COMPILED: 'not-compiled',
get(type) {
return compileStatusConstant[type] || compileStatusConstant.NOT_COMPILED;
}
BUILT_IDLE: 'idle',
BUILT_RUNNING: 'running',
BUILT_SUCCESS: 'success',
BUILT_FAILED: 'failed',
BUILT_CANCELED: 'canceled'
};
export const compileStatusDic = [
{
label: '编译中',
value: compileStatusConstant.BUILT_DOING
label: '空闲中',
value: compileStatusConstant.BUILT_IDLE,
color: 'blue'
},
{
label: '免编译',
value: compileStatusConstant.BUILT_IGNORE
label: '编译中',
value: compileStatusConstant.BUILT_RUNNING,
color: 'gray'
},
{
label: '已编译',
value: compileStatusConstant.BUILT_DOCKER
label: '编译成功',
value: compileStatusConstant.BUILT_SUCCESS,
color: 'green'
},
{
label: '编译失败',
value: compileStatusConstant.BUILT_FAIL
value: compileStatusConstant.BUILT_FAILED,
color: 'red'
},
{
label: '编译取消',
value: compileStatusConstant.BUILT_CANCELED,
color: 'orange'
}
];
export const compileStatusMap = startStatusDic.reduce((obj, item) => {

@ -58,7 +58,8 @@ const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
wsRef.current.send(messageStr);
} else {
}
else {
console.warn('WebSocket is not connected. Cannot send message.');
}
}, []);
@ -67,25 +68,27 @@ const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
const connect = useCallback((url: string) => {
// 先断开现有连接
disconnect();
urlRef.current = url;
try {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = (event) => {
console.log('WebSocket opened');
setReadyState(WebSocket.OPEN);
setIsConnected(true);
reconnectAttemptsRef.current = 0;
onOpen?.(event);
};
ws.onclose = (event) => {
console.log('WebSocket closed');
setReadyState(WebSocket.CLOSED);
setIsConnected(false);
onClose?.(event);
// 处理重连
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
@ -94,16 +97,18 @@ const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
}, reconnectInterval);
}
};
ws.onerror = (event) => {
console.log('WebSocket error', event);
setIsConnected(false);
onError?.(event);
};
ws.onmessage = (event) => {
console.log('WebSocket message', event);
onMessage?.(event);
};
setReadyState(WebSocket.CONNECTING);
} catch (error) {
console.error('Failed to create WebSocket connection:', error);

@ -25,9 +25,25 @@ interface CollapseListProps {
searchKeyword?: string;
runStatus?: string;
componentClassify?: string;
compileLogsMap: Record<string, string>;
compileStatusMap: Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>;
subscribeInstanceLog: (instanceId: string) => void;
unsubscribeInstanceLog: (instanceId: string) => void;
updateCompileStatus: (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => void;
isWsConnected: boolean;
}
const CollapseList: React.FC<CollapseListProps> = ({ searchKeyword, runStatus, componentClassify }) => {
const CollapseList: React.FC<CollapseListProps> = ({
searchKeyword,
runStatus,
componentClassify,
compileLogsMap,
compileStatusMap,
subscribeInstanceLog,
unsubscribeInstanceLog,
updateCompileStatus,
isWsConnected
}) => {
const [collapses, setCollapses] = useState([]);
const [visible, setVisible] = useState(false);
const [showOffSaleModal, setShowOffSaleModal] = useState(false);
@ -258,7 +274,15 @@ const CollapseList: React.FC<CollapseListProps> = ({ searchKeyword, runStatus, c
name={index.toString()}
extra={extraNode(item)}
>
<ListNode componentData={item} />
<ListNode
componentData={item}
compileLogsMap={compileLogsMap}
compileStatusMap={compileStatusMap}
subscribeInstanceLog={subscribeInstanceLog}
unsubscribeInstanceLog={unsubscribeInstanceLog}
updateCompileStatus={updateCompileStatus}
isWsConnected={isWsConnected}
/>
</CollapseItem>
))}
</Collapse>

@ -1,11 +1,13 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import styles from './style/index.module.less';
import { Button, Input, Modal, Radio, Space, Select } from '@arco-design/web-react';
import { Button, Input, Modal, Radio, Space, Select, Message } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import CollapseList from './collapseList';
import { startStatusConstant } from '@/const/isdp/componentDeploy';
import ConfigTutorial from '@/pages/componentDevelopment/componentEnv/configTutorial';
import { getComponentClassify } from '@/api/componentClassify';
import useWebSocket from '@/hooks/useWebSocket';
import { getToken } from '@/utils/auth';
const ComponentDeployment = () => {
const [searchKeyword, setSearchKeyword] = useState('');
@ -15,6 +17,58 @@ const ComponentDeployment = () => {
const [selectedStatus, setSelectedStatus] = useState<string | undefined>(undefined);
const [tutorialVisible, setTutorialVisible] = useState(false);
// 编译日志相关状态 - 按实例 ID 分组存储
const [compileLogsMap, setCompileLogsMap] = useState<Record<string, string>>({});
// 编译状态管理 - 按实例 ID 存储状态
const [compileStatusMap, setCompileStatusMap] = useState<Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>>({});
const currentCompileIdRef = useRef<string | null>(null);
// WebSocket 连接 - 用于接收编译日志
const { connect: wsConnect, disconnect: wsDisconnect, sendMessage, isConnected } = useWebSocket({
onMessage: (event) => {
const id = currentCompileIdRef.current;
if (!id) return;
try {
const data = typeof event.data === 'string' ? event.data : JSON.stringify(event.data);
setCompileLogsMap(prev => ({
...prev,
[id]: prev[id] ? prev[id] + '\n' + data : data
}));
} catch (error) {
console.error('解析 WebSocket 消息失败:', error);
}
},
onOpen: () => {
console.log('编译日志 WebSocket 连接成功');
},
onError: (error) => {
console.error('编译日志 WebSocket 错误:', error);
},
onClose: () => {
console.log('编译日志 WebSocket 连接已关闭');
}
});
// 页面挂载时连接 WebSocket
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
let wsUrl;
if (window.location.host.includes('localhost')) {
wsUrl = `${process.env.NEXT_PUBLIC_DEV_SOCKET_HOST}/ws/v1/bpms-workbench/instance-log?Authorization=Bearer ${getToken()}`;
} else {
wsUrl = `${protocol}//${host}/ws/v1/bpms-workbench/instance-log?Authorization=Bearer ${getToken()}`;
}
wsConnect(wsUrl);
return () => {
wsDisconnect();
};
}, []);
useEffect(() => {
const timer = setTimeout(() => setDebouncedKeyword(searchKeyword), 500);
return () => clearTimeout(timer);
@ -28,6 +82,54 @@ const ComponentDeployment = () => {
});
}, []);
// 订阅实例编译日志
const subscribeInstanceLog = (instanceId: string) => {
currentCompileIdRef.current = instanceId;
// 清空该实例之前的日志
setCompileLogsMap(prev => ({
...prev,
[instanceId]: ''
}));
// 设置编译状态为 running
setCompileStatusMap(prev => ({
...prev,
[instanceId]: 'running'
}));
if (isConnected) {
sendMessage({
instanceId,
action: 'subscribe'
});
} else {
Message.warning('WebSocket 未连接,无法实时接收日志');
}
};
// 取消订阅实例编译日志
const unsubscribeInstanceLog = (instanceId: string) => {
if (isConnected && instanceId) {
sendMessage({
instanceId,
action: 'unsubscribe'
});
}
if (currentCompileIdRef.current === instanceId) {
currentCompileIdRef.current = null;
}
};
// 更新编译状态
const updateCompileStatus = (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => {
setCompileStatusMap(prev => ({
...prev,
[instanceId]: status
}));
};
// 状态选项配置
const statusOptions = [
{ label: '全部', value: undefined },
@ -88,8 +190,17 @@ const ComponentDeployment = () => {
</Space>
</div>
<div className={styles['content']}>
<CollapseList searchKeyword={debouncedKeyword} runStatus={selectedStatus}
componentClassify={selectedClassify} />
<CollapseList
searchKeyword={debouncedKeyword}
runStatus={selectedStatus}
componentClassify={selectedClassify}
compileLogsMap={compileLogsMap}
compileStatusMap={compileStatusMap}
subscribeInstanceLog={subscribeInstanceLog}
unsubscribeInstanceLog={unsubscribeInstanceLog}
updateCompileStatus={updateCompileStatus}
isWsConnected={isConnected}
/>
</div>
</div>

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Space,
@ -22,20 +22,41 @@ import {
refreshInstanceDependency,
getComponentResource
} from '@/api/componentInstance';
import { runStatusConstant, runStatusDic, runTypeConstant, runTypeDic } from '@/const/isdp/componentDeploy';
import {
compileStatusDic,
runStatusConstant,
runStatusDic,
runTypeConstant,
runTypeDic
} from '@/const/isdp/componentDeploy';
import dayjs from 'dayjs';
import EditInstanceModal from './editInstanceModal';
import EnvConfigModal from './envConfigModal';
import ResourceMonitorModal from '@/components/ResourceMonitorModal';
import { getToken } from '@/utils/auth';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
interface ListNodeProps {
componentData: any; // 组件数据
compileLogsMap: Record<string, string>;
compileStatusMap: Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>;
subscribeInstanceLog: (instanceId: string) => void;
unsubscribeInstanceLog: (instanceId: string) => void;
updateCompileStatus: (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => void;
isWsConnected: boolean;
}
const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const ListNode: React.FC<ListNodeProps> = ({
componentData,
compileLogsMap,
compileStatusMap,
subscribeInstanceLog,
unsubscribeInstanceLog,
updateCompileStatus,
isWsConnected
}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshingIds, setRefreshingIds] = useState<Set<string>>(new Set()); // 记录正在刷新的实例ID
@ -65,6 +86,10 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const [resourceModalVisible, setResourceModalVisible] = useState(false);
const [resourceData, setResourceData] = useState<any>(null);
// 编译日志 Modal 相关状态
const [compileModalVisible, setCompileModalVisible] = useState(false);
const [compileInstance, setCompileInstance] = useState<any>(null);
// 获取实例列表
const fetchInstanceList = async () => {
if (!componentData?.identifier) return;
@ -164,35 +189,20 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
// 添加到刷新中的集合
setRefreshingIds(prev => new Set(prev).add(record.id));
// 显示提示消息
const messageKey = `refresh_${record.id}`;
Message.loading({
id: messageKey,
content: '正在编译组件,请稍候...',
duration: 0 // 不自动关闭
});
try {
// 调用编译接口
const res: any = await refreshInstanceDependency(record.id);
// 关闭 loading 消息
Message.clear();
if (res.code === 200 && res.data) {
Message.success('依赖刷新成功');
// 刷新列表
fetchInstanceList();
if (res.code === 200) {
Message.success('编译任务已提交');
}
else {
Message.error(res.msg || '依赖刷新失败');
Message.error(res.msg || '编译失败');
}
} catch (error) {
// 关闭 loading 消息
Message.clear();
console.error('编译组件失败:', error);
Message.error('依赖刷新失败,请稍后重试');
Message.error('编译组件失败,请稍后重试');
} finally {
// 从刷新中的集合移除
setRefreshingIds(prev => {
const newSet = new Set(prev);
newSet.delete(record.id);
@ -201,6 +211,27 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
}
};
// 打开编译日志 Modal
const handleOpenCompileLog = (record) => {
setCompileInstance(record);
setCompileModalVisible(true);
// 通过父组件传递的方法订阅该实例的编译日志
subscribeInstanceLog(record.id);
};
// 关闭编译日志 Modal
const handleCloseCompileModal = () => {
setCompileModalVisible(false);
// 取消订阅当前实例的日志
if (compileInstance?.id) {
unsubscribeInstanceLog(compileInstance.id);
}
setCompileInstance(null);
};
// 打开日志 Modal
const handleOpenLog = (record) => {
setCurrentInstance(record);
@ -319,7 +350,8 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
if (res.code === 200 && res.data) {
setResourceData(res.data);
setResourceModalVisible(true);
} else {
}
else {
Message.error(res.msg || '获取资源信息失败');
}
} catch (error) {
@ -338,7 +370,17 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
{
title: '组件标识',
dataIndex: 'identifier',
align: 'center'
align: 'center',
render: (identifier, record) => {
return (
<span
style={{ color: '#3491FA', cursor: 'pointer' }}
onClick={() => handleOpenEdit(record)}
>
{identifier}
</span>
);
}
},
{
title: '实例名称',
@ -355,7 +397,7 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
}
},
{
title: '运行状态',
title: '实例运行状态',
dataIndex: 'runStatus',
align: 'center',
render: (runStatus) => {
@ -363,6 +405,15 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
return item ? <Tag color={item.color}>{item.label}</Tag> : '-';
}
},
{
title: '实例编译状态',
dataIndex: 'currentTaskStatus',
align: 'center',
render: (currentTaskStatus) => {
const item = compileStatusDic.find(d => d.value === currentTaskStatus);
return item ? <Tag color={item.color}>{item.label}</Tag> : '-';
}
},
{
title: '创建时间',
dataIndex: 'createTime',
@ -381,6 +432,11 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const isRefreshing = refreshingIds.has(record.id);
const isLocalRun = record.runType === 'LOCAL';
// 获取当前实例的编译状态
const compileStatus = compileStatusMap[record.id] || 'idle';
// 编译状态为 running、failed、canceled 时不显示启停按钮
const hideStartStopButtons = compileStatus === 'running' || compileStatus === 'failed' || compileStatus === 'canceled';
return (
<div className={styles['table-handle-box']}>
<Space size={20}>
@ -394,6 +450,12 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
>
</Button>
<Button
type="text"
onClick={() => handleOpenCompileLog(record)}
>
</Button>
<Button type="text" onClick={() => handleOpenLog(record)}></Button>
</>
)}
@ -403,36 +465,34 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
{!isLocalRun && isRunning && (
<Button type="text" onClick={() => handeViewResource(record)}></Button>
)}
<Button type="text"
onClick={() => handleOpenEdit(record)}
{!hideStartStopButtons && (
<>
{isRunning ? (
<Button
type="text"
style={{ color: 'rgb(var(--danger-6))' }}
icon={<img
src={'/icons/powerUpIcon.png'}
style={{
width: 16,
height: 16,
marginRight: 5,
verticalAlign: 'middle',
filter: 'invert(27%) sepia(96%) saturate(4392%) hue-rotate(348deg) brightness(95%) contrast(95%)'
}} />}
onClick={() => handleStop(record)}
></Button>
) : (
<Button
type="text"
icon={<img
src={'/icons/editIcon.png'}
src={'/icons/powerUpIcon.png'}
style={{ width: 16, height: 16, marginRight: 5, verticalAlign: 'middle' }} />}
></Button>
{isRunning ? (
<Button
type="text"
style={{ color: 'rgb(var(--danger-6))' }}
icon={<img
src={'/icons/powerUpIcon.png'}
style={{
width: 16,
height: 16,
marginRight: 5,
verticalAlign: 'middle',
filter: 'invert(27%) sepia(96%) saturate(4392%) hue-rotate(348deg) brightness(95%) contrast(95%)'
}} />}
onClick={() => handleStop(record)}
></Button>
) : (
<Button
type="text"
icon={<img
src={'/icons/powerUpIcon.png'}
style={{ width: 16, height: 16, marginRight: 5, verticalAlign: 'middle' }} />}
onClick={() => handleStart(record)}
loading={startingId === record.id && startLoading}
></Button>
onClick={() => handleStart(record)}
loading={startingId === record.id && startLoading}
></Button>
)}
</>
)}
{!isRunning && (
<Button
@ -518,6 +578,26 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
/>
</Modal>
{/* 编译日志 Modal */}
<Modal
title={`编译日志 - ${compileInstance?.name || compileInstance?.identifier || ''}`}
visible={compileModalVisible}
onCancel={handleCloseCompileModal}
footer={null}
style={{ width: '70%' }}
>
<TextArea
value={compileLogsMap[compileInstance?.id] || ''}
readOnly
placeholder="等待编译输出..."
style={{
minHeight: 400,
fontFamily: 'monospace',
fontSize: 13
}}
/>
</Modal>
{/* 编辑实例信息 Modal */}
<EditInstanceModal
visible={editModalVisible}

Loading…
Cancel
Save