|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
import { |
|
|
Modal, |
|
|
Typography, |
|
|
Card, |
|
|
Tag, |
|
|
Progress, |
|
|
Descriptions, |
|
|
Spin, |
|
|
Empty, |
|
|
Button, |
|
|
Badge, |
|
|
Tooltip, |
|
|
} from '@douyinfe/semi-ui'; |
|
|
import { |
|
|
FaInfoCircle, |
|
|
FaServer, |
|
|
FaClock, |
|
|
FaMapMarkerAlt, |
|
|
FaDocker, |
|
|
FaMoneyBillWave, |
|
|
FaChartLine, |
|
|
FaCopy, |
|
|
FaLink, |
|
|
} from 'react-icons/fa'; |
|
|
import { IconRefresh } from '@douyinfe/semi-icons'; |
|
|
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers'; |
|
|
|
|
|
const { Text, Title } = Typography; |
|
|
|
|
|
const ViewDetailsModal = ({ |
|
|
visible, |
|
|
onCancel, |
|
|
deployment, |
|
|
t |
|
|
}) => { |
|
|
const [details, setDetails] = useState(null); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [containers, setContainers] = useState([]); |
|
|
const [containersLoading, setContainersLoading] = useState(false); |
|
|
|
|
|
const fetchDetails = async () => { |
|
|
if (!deployment?.id) return; |
|
|
|
|
|
setLoading(true); |
|
|
try { |
|
|
const response = await API.get(`/api/deployments/${deployment.id}`); |
|
|
if (response.data.success) { |
|
|
setDetails(response.data.data); |
|
|
} |
|
|
} catch (error) { |
|
|
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message)); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const fetchContainers = async () => { |
|
|
if (!deployment?.id) return; |
|
|
|
|
|
setContainersLoading(true); |
|
|
try { |
|
|
const response = await API.get(`/api/deployments/${deployment.id}/containers`); |
|
|
if (response.data.success) { |
|
|
setContainers(response.data.data?.containers || []); |
|
|
} |
|
|
} catch (error) { |
|
|
showError(t('获取容器信息失败') + ': ' + (error.response?.data?.message || error.message)); |
|
|
} finally { |
|
|
setContainersLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
if (visible && deployment?.id) { |
|
|
fetchDetails(); |
|
|
fetchContainers(); |
|
|
} else if (!visible) { |
|
|
setDetails(null); |
|
|
setContainers([]); |
|
|
} |
|
|
}, [visible, deployment?.id]); |
|
|
|
|
|
const handleCopyId = () => { |
|
|
navigator.clipboard.writeText(deployment?.id); |
|
|
showSuccess(t('ID已复制到剪贴板')); |
|
|
}; |
|
|
|
|
|
const handleRefresh = () => { |
|
|
fetchDetails(); |
|
|
fetchContainers(); |
|
|
}; |
|
|
|
|
|
const getStatusConfig = (status) => { |
|
|
const statusConfig = { |
|
|
'running': { color: 'green', text: '运行中', icon: '🟢' }, |
|
|
'completed': { color: 'green', text: '已完成', icon: '✅' }, |
|
|
'deployment requested': { color: 'blue', text: '部署请求中', icon: '🔄' }, |
|
|
'termination requested': { color: 'orange', text: '终止请求中', icon: '⏸️' }, |
|
|
'destroyed': { color: 'red', text: '已销毁', icon: '🔴' }, |
|
|
'failed': { color: 'red', text: '失败', icon: '❌' } |
|
|
}; |
|
|
return statusConfig[status] || { color: 'grey', text: status, icon: '❓' }; |
|
|
}; |
|
|
|
|
|
const statusConfig = getStatusConfig(deployment?.status); |
|
|
|
|
|
return ( |
|
|
<Modal |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaInfoCircle className="text-blue-500" /> |
|
|
<span>{t('容器详情')}</span> |
|
|
</div> |
|
|
} |
|
|
visible={visible} |
|
|
onCancel={onCancel} |
|
|
footer={ |
|
|
<div className="flex justify-between"> |
|
|
<Button |
|
|
icon={<IconRefresh />} |
|
|
onClick={handleRefresh} |
|
|
loading={loading || containersLoading} |
|
|
theme="borderless" |
|
|
> |
|
|
{t('刷新')} |
|
|
</Button> |
|
|
<Button onClick={onCancel}> |
|
|
{t('关闭')} |
|
|
</Button> |
|
|
</div> |
|
|
} |
|
|
width={800} |
|
|
className="deployment-details-modal" |
|
|
> |
|
|
{loading && !details ? ( |
|
|
<div className="flex items-center justify-center py-12"> |
|
|
<Spin size="large" tip={t('加载详情中...')} /> |
|
|
</div> |
|
|
) : details ? ( |
|
|
<div className="space-y-4 max-h-[600px] overflow-y-auto"> |
|
|
{/* Basic Info */} |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaServer className="text-blue-500" /> |
|
|
<span>{t('基本信息')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<Descriptions data={[ |
|
|
{ |
|
|
key: t('容器名称'), |
|
|
value: ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<Text strong className="text-base"> |
|
|
{details.deployment_name || details.id} |
|
|
</Text> |
|
|
<Button |
|
|
size="small" |
|
|
theme="borderless" |
|
|
icon={<FaCopy />} |
|
|
onClick={handleCopyId} |
|
|
className="opacity-70 hover:opacity-100" |
|
|
/> |
|
|
</div> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('容器ID'), |
|
|
value: ( |
|
|
<Text type="secondary" className="font-mono text-sm"> |
|
|
{details.id} |
|
|
</Text> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('状态'), |
|
|
value: ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<span>{statusConfig.icon}</span> |
|
|
<Tag color={statusConfig.color}> |
|
|
{t(statusConfig.text)} |
|
|
</Tag> |
|
|
</div> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('创建时间'), |
|
|
value: timestamp2string(details.created_at) |
|
|
} |
|
|
]} /> |
|
|
</Card> |
|
|
|
|
|
{/* Hardware & Performance */} |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaChartLine className="text-green-500" /> |
|
|
<span>{t('硬件与性能')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<div className="space-y-4"> |
|
|
<Descriptions data={[ |
|
|
{ |
|
|
key: t('硬件类型'), |
|
|
value: ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<Tag color="blue">{details.brand_name}</Tag> |
|
|
<Text strong>{details.hardware_name}</Text> |
|
|
</div> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('GPU数量'), |
|
|
value: ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<Badge count={details.total_gpus} theme="solid" type="primary"> |
|
|
<FaServer className="text-purple-500" /> |
|
|
</Badge> |
|
|
<Text>{t('总计')} {details.total_gpus} {t('个GPU')}</Text> |
|
|
</div> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('容器配置'), |
|
|
value: ( |
|
|
<div className="space-y-1"> |
|
|
<div>{t('每容器GPU数')}: {details.gpus_per_container}</div> |
|
|
<div>{t('容器总数')}: {details.total_containers}</div> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
]} /> |
|
|
|
|
|
{/* Progress Bar */} |
|
|
<div className="space-y-2"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<Text strong>{t('完成进度')}</Text> |
|
|
<Text>{details.completed_percent}%</Text> |
|
|
</div> |
|
|
<Progress |
|
|
percent={details.completed_percent} |
|
|
status={details.completed_percent === 100 ? 'success' : 'normal'} |
|
|
strokeWidth={8} |
|
|
showInfo={false} |
|
|
/> |
|
|
<div className="flex justify-between text-xs text-gray-500"> |
|
|
<span>{t('已服务')}: {details.compute_minutes_served} {t('分钟')}</span> |
|
|
<span>{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</Card> |
|
|
|
|
|
{/* Container Configuration */} |
|
|
{details.container_config && ( |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaDocker className="text-blue-600" /> |
|
|
<span>{t('容器配置')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<div className="space-y-3"> |
|
|
<Descriptions data={[ |
|
|
{ |
|
|
key: t('镜像地址'), |
|
|
value: ( |
|
|
<Text className="font-mono text-sm break-all"> |
|
|
{details.container_config.image_url || 'N/A'} |
|
|
</Text> |
|
|
) |
|
|
}, |
|
|
{ |
|
|
key: t('流量端口'), |
|
|
value: details.container_config.traffic_port || 'N/A' |
|
|
}, |
|
|
{ |
|
|
key: t('启动命令'), |
|
|
value: ( |
|
|
<Text className="font-mono text-sm"> |
|
|
{details.container_config.entrypoint ? |
|
|
details.container_config.entrypoint.join(' ') : 'N/A' |
|
|
} |
|
|
</Text> |
|
|
) |
|
|
} |
|
|
]} /> |
|
|
|
|
|
{/* Environment Variables */} |
|
|
{details.container_config.env_variables && |
|
|
Object.keys(details.container_config.env_variables).length > 0 && ( |
|
|
<div className="mt-4"> |
|
|
<Text strong className="block mb-2">{t('环境变量')}:</Text> |
|
|
<div className="bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto"> |
|
|
{Object.entries(details.container_config.env_variables).map(([key, value]) => ( |
|
|
<div key={key} className="flex gap-2 text-sm font-mono mb-1"> |
|
|
<span className="text-blue-600 font-medium">{key}=</span> |
|
|
<span className="text-gray-700 break-all">{String(value)}</span> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</Card> |
|
|
)} |
|
|
|
|
|
{/* Containers List */} |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaServer className="text-indigo-500" /> |
|
|
<span>{t('容器实例')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
{containersLoading ? ( |
|
|
<div className="flex items-center justify-center py-6"> |
|
|
<Spin tip={t('加载容器信息中...')} /> |
|
|
</div> |
|
|
) : containers.length === 0 ? ( |
|
|
<Empty description={t('暂无容器信息')} image={Empty.PRESENTED_IMAGE_SIMPLE} /> |
|
|
) : ( |
|
|
<div className="space-y-3"> |
|
|
{containers.map((ctr) => ( |
|
|
<Card |
|
|
key={ctr.container_id} |
|
|
className="bg-gray-50 border border-gray-100" |
|
|
bodyStyle={{ padding: '12px 16px' }} |
|
|
> |
|
|
<div className="flex flex-wrap items-center justify-between gap-3"> |
|
|
<div className="flex flex-col gap-1"> |
|
|
<Text strong className="font-mono text-sm"> |
|
|
{ctr.container_id} |
|
|
</Text> |
|
|
<Text size="small" type="secondary"> |
|
|
{t('设备')} {ctr.device_id || '--'} · {t('状态')} {ctr.status || '--'} |
|
|
</Text> |
|
|
<Text size="small" type="secondary"> |
|
|
{t('创建时间')}: {ctr.created_at ? timestamp2string(ctr.created_at) : '--'} |
|
|
</Text> |
|
|
</div> |
|
|
<div className="flex flex-col items-end gap-2"> |
|
|
<Tag color="blue" size="small"> |
|
|
{t('GPU/容器')}: {ctr.gpus_per_container ?? '--'} |
|
|
</Tag> |
|
|
{ctr.public_url && ( |
|
|
<Tooltip content={ctr.public_url}> |
|
|
<Button |
|
|
icon={<FaLink />} |
|
|
size="small" |
|
|
theme="light" |
|
|
onClick={() => window.open(ctr.public_url, '_blank', 'noopener,noreferrer')} |
|
|
> |
|
|
{t('访问容器')} |
|
|
</Button> |
|
|
</Tooltip> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{ctr.events && ctr.events.length > 0 && ( |
|
|
<div className="mt-3 bg-white rounded-md border border-gray-100 p-3"> |
|
|
<Text size="small" type="secondary" className="block mb-2"> |
|
|
{t('最近事件')} |
|
|
</Text> |
|
|
<div className="space-y-2 max-h-32 overflow-y-auto"> |
|
|
{ctr.events.map((event, index) => ( |
|
|
<div key={`${ctr.container_id}-${event.time}-${index}`} className="flex gap-3 text-xs font-mono"> |
|
|
<span className="text-gray-500 min-w-[140px]"> |
|
|
{event.time ? timestamp2string(event.time) : '--'} |
|
|
</span> |
|
|
<span className="text-gray-700 break-all flex-1"> |
|
|
{event.message || '--'} |
|
|
</span> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</Card> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
</Card> |
|
|
|
|
|
{/* Location Information */} |
|
|
{details.locations && details.locations.length > 0 && ( |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaMapMarkerAlt className="text-orange-500" /> |
|
|
<span>{t('部署位置')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<div className="flex flex-wrap gap-2"> |
|
|
{details.locations.map((location) => ( |
|
|
<Tag key={location.id} color="orange" size="large"> |
|
|
<div className="flex items-center gap-1"> |
|
|
<span>🌍</span> |
|
|
<span>{location.name} ({location.iso2})</span> |
|
|
</div> |
|
|
</Tag> |
|
|
))} |
|
|
</div> |
|
|
</Card> |
|
|
)} |
|
|
|
|
|
{/* Cost Information */} |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaMoneyBillWave className="text-green-500" /> |
|
|
<span>{t('费用信息')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<div className="space-y-3"> |
|
|
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg"> |
|
|
<Text>{t('已支付金额')}</Text> |
|
|
<Text strong className="text-lg text-green-600"> |
|
|
${details.amount_paid ? details.amount_paid.toFixed(2) : '0.00'} USDC |
|
|
</Text> |
|
|
</div> |
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm"> |
|
|
<div className="flex justify-between"> |
|
|
<Text type="secondary">{t('计费开始')}:</Text> |
|
|
<Text>{details.started_at ? timestamp2string(details.started_at) : 'N/A'}</Text> |
|
|
</div> |
|
|
<div className="flex justify-between"> |
|
|
<Text type="secondary">{t('预计结束')}:</Text> |
|
|
<Text>{details.finished_at ? timestamp2string(details.finished_at) : 'N/A'}</Text> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</Card> |
|
|
|
|
|
{/* Time Information */} |
|
|
<Card |
|
|
title={ |
|
|
<div className="flex items-center gap-2"> |
|
|
<FaClock className="text-purple-500" /> |
|
|
<span>{t('时间信息')}</span> |
|
|
</div> |
|
|
} |
|
|
className="border-0 shadow-sm" |
|
|
> |
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
|
<div className="space-y-2"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<Text type="secondary">{t('已运行时间')}:</Text> |
|
|
<Text strong> |
|
|
{Math.floor(details.compute_minutes_served / 60)}h {details.compute_minutes_served % 60}m |
|
|
</Text> |
|
|
</div> |
|
|
<div className="flex items-center justify-between"> |
|
|
<Text type="secondary">{t('剩余时间')}:</Text> |
|
|
<Text strong className="text-orange-600"> |
|
|
{Math.floor(details.compute_minutes_remaining / 60)}h {details.compute_minutes_remaining % 60}m |
|
|
</Text> |
|
|
</div> |
|
|
</div> |
|
|
<div className="space-y-2"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<Text type="secondary">{t('创建时间')}:</Text> |
|
|
<Text>{timestamp2string(details.created_at)}</Text> |
|
|
</div> |
|
|
<div className="flex items-center justify-between"> |
|
|
<Text type="secondary">{t('最后更新')}:</Text> |
|
|
<Text>{timestamp2string(details.updated_at)}</Text> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</Card> |
|
|
</div> |
|
|
) : ( |
|
|
<Empty |
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE} |
|
|
description={t('无法获取容器详情')} |
|
|
/> |
|
|
)} |
|
|
</Modal> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default ViewDetailsModal; |
|
|
|