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.

470 lines
13 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Button,
Space,
Table,
TableColumnProps,
Tag,
Message,
Modal,
DatePicker,
InputNumber,
Input,
Grid
} from '@arco-design/web-react';
import styles from '@/pages/componentDevelopment/componentDeployment/style/collapseList.module.less';
import {
deleteInstance,
getInstanceList,
startInstance,
stopInstance,
getInstanceLog,
refreshInstanceDependency
} from '@/api/componentInstance';
import { runStatusConstant, runStatusDic, runTypeConstant, runTypeDic } from '@/const/isdp/componentDeploy';
import dayjs from 'dayjs';
import EditInstanceModal from './editInstanceModal';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
interface ListNodeProps {
componentData: any; // 组件数据
}
const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshingIds, setRefreshingIds] = useState<Set<string>>(new Set()); // 记录正在刷新的实例ID
// 日志 Modal 相关状态
const [logModalVisible, setLogModalVisible] = useState(false);
const [currentInstance, setCurrentInstance] = useState<any>(null);
const [logTimeRange, setLogTimeRange] = useState<any[]>([
dayjs().startOf('day'), // 当天0点
dayjs().endOf('day') // 当天24点
]);
const [logLines, setLogLines] = useState<number>(100);
const [logContent, setLogContent] = useState<string>('');
const [logLoading, setLogLoading] = useState(false);
// 编辑实例 Modal 相关状态
const [editModalVisible, setEditModalVisible] = useState(false);
const [editingInstance, setEditingInstance] = useState<any>(null);
// 获取实例列表
const fetchInstanceList = async () => {
if (!componentData?.identifier) return;
setLoading(true);
try {
const params = {
current: 1,
size: 10
};
const res: any = await getInstanceList(params, componentData.identifier);
if (res.code === 200) {
setData(res.data.records || []);
}
} catch (error) {
console.error('获取实例列表失败:', error);
} finally {
setLoading(false);
}
};
// 监听 componentData 变化,调用接口
useEffect(() => {
fetchInstanceList();
}, [componentData]);
// 处理启动实例
const handleStart = async (record) => {
try {
// 先执行刷新依赖
await refreshInstanceDependency(record.id);
// 刷新依赖完成后启动
const res: any = await startInstance(record.id);
if (res.code === 200) {
Message.success('启动成功');
fetchInstanceList(); // 刷新列表
}
else {
Message.error(res.msg || '启动失败');
}
} catch (error) {
console.error('启动实例失败:', error);
Message.error('启动失败');
}
};
// 处理停止实例
const handleStop = async (record) => {
try {
const res: any = await stopInstance(record.id);
if (res.code === 200) {
Message.success('停止成功');
fetchInstanceList(); // 刷新列表
}
else {
Message.error(res.msg || '停止失败');
}
} catch (error) {
console.error('停止实例失败:', error);
Message.error('停止失败');
}
};
// 删除实例
const handleRemove = async (record) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除实例"${record.name || record.identifier}"吗?此操作不可恢复。`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const res: any = await deleteInstance(record.id);
if (res.code === 200) {
Message.success('删除成功');
fetchInstanceList(); // 刷新列表
}
else {
Message.error(res.msg || '删除失败');
}
} catch (error) {
console.error('删除实例失败:', error);
Message.error('删除失败');
}
}
});
};
// 刷新依赖
const handleRefresh = async (record) => {
// 添加到刷新中的集合
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) {
Message.success('依赖刷新成功');
// 刷新列表
fetchInstanceList();
}
else {
Message.error(res.msg || '依赖刷新失败');
}
} catch (error) {
// 关闭 loading 消息
Message.clear();
console.error('刷新依赖失败:', error);
Message.error('依赖刷新失败,请稍后重试');
} finally {
// 从刷新中的集合移除
setRefreshingIds(prev => {
const newSet = new Set(prev);
newSet.delete(record.id);
return newSet;
});
}
};
// 打开日志 Modal
const handleOpenLog = (record) => {
setCurrentInstance(record);
setLogModalVisible(true);
// 打开时自动查询一次
fetchLog(record.id);
};
// 查询日志
const fetchLog = async (instanceId?: string) => {
const id = instanceId || currentInstance?.id;
if (!id) return;
setLogLoading(true);
try {
const params: any = {
id,
tail: logLines
};
// 如果选择了时间范围,转换为时间戳(秒)
if (logTimeRange && logTimeRange.length === 2) {
params.since = dayjs(logTimeRange[0]).unix();
params.until = dayjs(logTimeRange[1]).unix();
}
const res: any = await getInstanceLog(params);
if (res.code === 200) {
const logs = res.data.result || [];
if (Array.isArray(logs) && logs.length > 0) {
// 将字符串数组转换为带行号的文本
const logText = logs.map((line, index) => `${index + 1} ${line}`).join('\n');
setLogContent(logText);
}
else {
setLogContent('暂无日志');
}
}
else {
Message.error(res.msg || '获取日志失败');
setLogContent('');
}
} catch (error) {
console.error('获取日志失败:', error);
Message.error('获取日志失败');
setLogContent('');
} finally {
setLogLoading(false);
}
};
// 重置日志查询条件
const handleResetLog = () => {
setLogTimeRange([
dayjs().startOf('day'), // 当天0点
dayjs().endOf('day') // 当天24点
]);
setLogLines(100);
setLogContent('');
};
// 关闭日志 Modal
const handleCloseLogModal = () => {
setLogModalVisible(false);
setCurrentInstance(null);
handleResetLog();
};
// 打开编辑实例 Modal
const handleOpenEdit = (record: any) => {
setEditingInstance(record);
setEditModalVisible(true);
};
// 处理编辑实例确定
const handleEditOk = async (values: any) => {
try {
// TODO: 调用更新实例接口
console.log('更新实例信息:', { ...editingInstance, ...values });
Message.success('更新成功');
setEditModalVisible(false);
setEditingInstance(null);
fetchInstanceList(); // 刷新列表
} catch (error) {
console.error('更新实例失败:', error);
Message.error('更新失败');
}
};
// 关闭编辑实例 Modal
const handleEditCancel = () => {
setEditModalVisible(false);
setEditingInstance(null);
};
const columns: TableColumnProps[] = [
{
title: '#',
dataIndex: 'key',
align: 'center',
render: (col, record, index) => index + 1
},
{
title: '组件标识',
dataIndex: 'identifier',
align: 'center'
},
{
title: '实例名称',
dataIndex: 'name',
align: 'center'
},
{
title: '运行类型',
dataIndex: 'runType',
align: 'center',
render: (runType) => {
const item = runTypeDic.find(d => d.value === runTypeConstant[runType]);
return item ? item.label : '-';
}
},
{
title: '运行状态',
dataIndex: 'runStatus',
align: 'center',
render: (runStatus) => {
const item = runStatusDic.find(d => d.value === runStatus);
return item ? <Tag color={item.color}>{item.label}</Tag> : '-';
}
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
render: (createTime) => {
return createTime ? dayjs(createTime).format('YYYY-MM-DD HH:mm:ss') : '-';
}
},
{
title: '操作',
dataIndex: '',
align: 'center',
render: (col, record, index) => {
// 根据运行状态判断显示启动还是停止按钮
const isRunning = record.runStatus === runStatusConstant.RUN || record.runStatus === runStatusConstant.HEALTHY;
const isRefreshing = refreshingIds.has(record.id);
return (
<div className={styles['table-handle-box']}>
<Space size={20}>
<Button
type="text"
onClick={() => handleRefresh(record)}
loading={isRefreshing}
disabled={isRefreshing}
>
</Button>
<Button type="text" onClick={() => handleOpenLog(record)}></Button>
<Button type="text"></Button>
<Button type="text"
onClick={() => handleOpenEdit(record)}
icon={<img
src={'/icons/editIcon.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)}
></Button>
)}
{!isRunning && (
<Button
type="text"
icon={<img
src={'/icons/deleteIcon.png'}
style={{ width: 16, height: 16, marginRight: 5, verticalAlign: 'middle' }} />}
onClick={() => handleRemove(record)}
></Button>
)}
</Space>
</div>
);
}
}
];
return (
<>
<Table
loading={loading}
pagination={false}
border={false}
columns={columns}
data={data}
/>
{/* 运行日志 Modal */}
<Modal
title={`运行日志 - ${currentInstance?.name || currentInstance?.identifier || ''}`}
visible={logModalVisible}
onCancel={handleCloseLogModal}
footer={null}
style={{ width: '70%' }}
>
<div style={{ marginBottom: 16 }}>
<Grid.Row gutter={12} align="center">
<Grid.Col span={10}>
<Space>
<span>:</span>
<RangePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
placeholder={['开始时间', '结束时间']}
value={logTimeRange}
onChange={setLogTimeRange}
/>
</Space>
</Grid.Col>
<Grid.Col span={12}>
<Space>
<span>:</span>
<InputNumber
placeholder="日志行数"
value={logLines}
onChange={setLogLines}
min={1}
max={10000}
style={{ width: 150 }}
/>
<Button
type="primary"
onClick={() => fetchLog()}
loading={logLoading}
>
</Button>
<Button onClick={handleResetLog}></Button>
</Space>
</Grid.Col>
</Grid.Row>
</div>
<TextArea
value={logContent}
readOnly
placeholder="暂无日志"
style={{
minHeight: 400,
fontFamily: 'monospace',
fontSize: 13
}}
/>
</Modal>
{/* 编辑实例信息 Modal */}
<EditInstanceModal
visible={editModalVisible}
instanceData={editingInstance}
onCancel={handleEditCancel}
onOk={handleEditOk}
/>
</>
);
};
export default ListNode;