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
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; |