You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
7.7 KiB
TypeScript
241 lines
7.7 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import styles from './style/index.module.less';
|
|
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';
|
|
import { isJSON } from '@/utils/common';
|
|
|
|
const ComponentDeployment = () => {
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
const [debouncedKeyword, setDebouncedKeyword] = useState('');
|
|
const [selectedClassify, setSelectedClassify] = useState<string | undefined>(undefined);
|
|
const [classifyOptions, setClassifyOptions] = useState<{ label: string; value: string }[]>([]);
|
|
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 compileLogSequenceRef = useRef<Record<string, number>>({});
|
|
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 currentSequence = (compileLogSequenceRef.current[id] || 0) + 1;
|
|
compileLogSequenceRef.current[id] = currentSequence;
|
|
|
|
if (isJSON(event.data)) {
|
|
const parseData = JSON.parse(event.data);
|
|
const message = parseData.message || parseData.line || event.data;
|
|
const sequenceMessage = `${currentSequence}. ${message}`;
|
|
setCompileLogsMap(prev => ({
|
|
...prev,
|
|
[id]: prev[id] ? prev[id] + '\n' + sequenceMessage : sequenceMessage
|
|
}));
|
|
}
|
|
else {
|
|
const data = `${currentSequence}. ${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);
|
|
}, [searchKeyword]);
|
|
|
|
useEffect(() => {
|
|
getComponentClassify('component').then((res: any) => {
|
|
if (res.code === 200) {
|
|
setClassifyOptions(res.data.map((item) => ({ label: item.classifyName, value: item.classifyName })));
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
// 订阅实例编译日志
|
|
const subscribeInstanceLog = (instanceId: string) => {
|
|
currentCompileIdRef.current = instanceId;
|
|
|
|
compileLogSequenceRef.current[instanceId] = 0;
|
|
|
|
// 清空该实例之前的日志
|
|
setCompileLogsMap(prev => ({
|
|
...prev,
|
|
[instanceId]: ''
|
|
}));
|
|
|
|
// 设置编译状态为 running
|
|
setCompileStatusMap(prev => ({
|
|
...prev,
|
|
[instanceId]: 'running'
|
|
}));
|
|
|
|
if (isConnected) {
|
|
sendMessage({
|
|
instanceId,
|
|
type: 'subscribe'
|
|
});
|
|
}
|
|
else {
|
|
Message.warning('WebSocket 未连接,无法实时接收日志');
|
|
}
|
|
};
|
|
|
|
// 取消订阅实例编译日志
|
|
const unsubscribeInstanceLog = (instanceId: string) => {
|
|
if (isConnected && instanceId) {
|
|
sendMessage({
|
|
instanceId,
|
|
type: '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 },
|
|
{ label: '启用中', value: 'RUN' },
|
|
{ label: '已下架', value: 'STOP' }
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className={styles['component-deployment']}>
|
|
<div className={styles['header']}>
|
|
<Radio.Group
|
|
value={selectedStatus}
|
|
onChange={(value) => setSelectedStatus(value)}
|
|
name="button-radio-group"
|
|
>
|
|
{statusOptions.map((item) => {
|
|
return (
|
|
<Radio key={item.label} value={item.value}>
|
|
{({ checked }) => {
|
|
return (
|
|
<Button tabIndex={-1} key={item.label} shape="round" type={checked ? 'primary' : 'default'}>
|
|
{item.label}
|
|
</Button>
|
|
);
|
|
}}
|
|
</Radio>
|
|
);
|
|
})}
|
|
</Radio.Group>
|
|
|
|
<Space>
|
|
<Select
|
|
placeholder="组件分类"
|
|
allowClear
|
|
style={{ width: 160 }}
|
|
value={selectedClassify}
|
|
onChange={(value) => setSelectedClassify(value)}
|
|
options={classifyOptions}
|
|
/>
|
|
<Input
|
|
prefix={<IconSearch />}
|
|
placeholder={'搜索'}
|
|
style={{ width: 236 }}
|
|
value={searchKeyword}
|
|
onChange={(value) => setSearchKeyword(value)}
|
|
onPressEnter={() => {
|
|
// 触发搜索
|
|
}}
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
style={{ marginLeft: 5, borderRadius: 4 }}
|
|
onClick={() => setTutorialVisible(true)}
|
|
>
|
|
实例指引
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
<div className={styles['content']}>
|
|
<CollapseList
|
|
searchKeyword={debouncedKeyword}
|
|
runStatus={selectedStatus}
|
|
componentClassify={selectedClassify}
|
|
compileLogsMap={compileLogsMap}
|
|
compileStatusMap={compileStatusMap}
|
|
subscribeInstanceLog={subscribeInstanceLog}
|
|
unsubscribeInstanceLog={unsubscribeInstanceLog}
|
|
updateCompileStatus={updateCompileStatus}
|
|
isWsConnected={isConnected}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal
|
|
title="组件实例教程"
|
|
visible={tutorialVisible}
|
|
onCancel={() => setTutorialVisible(false)}
|
|
footer={null}
|
|
style={{ top: 20, width: '100%' }}
|
|
>
|
|
<ConfigTutorial tutorialType="compInstance" />
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ComponentDeployment;
|