fix: fix model deployment style issues, lint problems, and i18n gaps. (#2556)
* fix: fix model deployment style issues, lint problems, and i18n gaps. * fix: adjust the key not to be displayed on the frontend, tested via the backend. * fix: adjust the sidebar configuration logic to use the default configuration items if they are not defined.
This commit is contained in:
@@ -30,30 +30,24 @@ import {
|
||||
Spin,
|
||||
Popconfirm,
|
||||
Tag,
|
||||
Avatar,
|
||||
Empty,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
Progress,
|
||||
Checkbox,
|
||||
Radio,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconClose,
|
||||
IconDownload,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconServer,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import {
|
||||
API,
|
||||
authHeader,
|
||||
getUserIdFromLocalStorage,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
} from '../../../../helpers';
|
||||
|
||||
@@ -85,9 +79,7 @@ const resolveOllamaBaseUrl = (info) => {
|
||||
}
|
||||
|
||||
const alt =
|
||||
typeof info.ollama_base_url === 'string'
|
||||
? info.ollama_base_url.trim()
|
||||
: '';
|
||||
typeof info.ollama_base_url === 'string' ? info.ollama_base_url.trim() : '';
|
||||
if (alt) {
|
||||
return alt;
|
||||
}
|
||||
@@ -125,7 +117,8 @@ const normalizeModels = (items) => {
|
||||
}
|
||||
|
||||
if (typeof item === 'object') {
|
||||
const candidateId = item.id || item.ID || item.name || item.model || item.Model;
|
||||
const candidateId =
|
||||
item.id || item.ID || item.name || item.model || item.Model;
|
||||
if (!candidateId) {
|
||||
return null;
|
||||
}
|
||||
@@ -147,7 +140,10 @@ const normalizeModels = (items) => {
|
||||
if (!normalized.digest && typeof metadata.digest === 'string') {
|
||||
normalized.digest = metadata.digest;
|
||||
}
|
||||
if (!normalized.modified_at && typeof metadata.modified_at === 'string') {
|
||||
if (
|
||||
!normalized.modified_at &&
|
||||
typeof metadata.modified_at === 'string'
|
||||
) {
|
||||
normalized.modified_at = metadata.modified_at;
|
||||
}
|
||||
if (metadata.details && !normalized.details) {
|
||||
@@ -440,7 +436,6 @@ const OllamaModelModal = ({
|
||||
};
|
||||
|
||||
await processStream();
|
||||
|
||||
} catch (error) {
|
||||
if (error?.name !== 'AbortError') {
|
||||
showError(t('模型拉取失败: {{error}}', { error: error.message }));
|
||||
@@ -461,7 +456,7 @@ const OllamaModelModal = ({
|
||||
model_name: modelName,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (res.data.success) {
|
||||
showSuccess(t('模型删除成功'));
|
||||
await fetchModels(); // 重新获取模型列表
|
||||
@@ -481,8 +476,8 @@ const OllamaModelModal = ({
|
||||
if (!searchValue) {
|
||||
setFilteredModels(models);
|
||||
} else {
|
||||
const filtered = models.filter(model =>
|
||||
model.id.toLowerCase().includes(searchValue.toLowerCase())
|
||||
const filtered = models.filter((model) =>
|
||||
model.id.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
);
|
||||
setFilteredModels(filtered);
|
||||
}
|
||||
@@ -527,60 +522,38 @@ const OllamaModelModal = ({
|
||||
const formatModelSize = (size) => {
|
||||
if (!size) return '-';
|
||||
const gb = size / (1024 * 1024 * 1024);
|
||||
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
return gb >= 1
|
||||
? `${gb.toFixed(1)} GB`
|
||||
: `${(size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<Avatar
|
||||
size='small'
|
||||
color='blue'
|
||||
className='mr-3 shadow-md'
|
||||
>
|
||||
<IconServer size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Title heading={4} className='m-0'>
|
||||
{t('Ollama 模型管理')}
|
||||
</Title>
|
||||
<Text type='tertiary' size='small'>
|
||||
{channelInfo?.name && `${channelInfo.name} - `}
|
||||
{t('管理 Ollama 模型的拉取和删除')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
title={t('Ollama 模型管理')}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
width={800}
|
||||
width={720}
|
||||
style={{ maxWidth: '95vw' }}
|
||||
footer={
|
||||
<div className='flex justify-end'>
|
||||
<Button
|
||||
theme='light'
|
||||
type='primary'
|
||||
onClick={onCancel}
|
||||
icon={<IconClose />}
|
||||
>
|
||||
{t('关闭')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button theme='solid' type='primary' onClick={onCancel}>
|
||||
{t('关闭')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
<Space vertical spacing='medium' style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{channelInfo?.name ? `${channelInfo.name} - ` : ''}
|
||||
{t('管理 Ollama 模型的拉取和删除')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 拉取新模型 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='green' className='mr-2'>
|
||||
<IconPlus size={16} />
|
||||
</Avatar>
|
||||
<Title heading={5} className='m-0'>
|
||||
{t('拉取新模型')}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Title heading={6} className='m-0 mb-3'>
|
||||
{t('拉取新模型')}
|
||||
</Title>
|
||||
|
||||
<Row gutter={12} align='middle'>
|
||||
<Col span={16}>
|
||||
<Input
|
||||
@@ -606,76 +579,81 @@ const OllamaModelModal = ({
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
{/* 进度条显示 */}
|
||||
{pullProgress && (() => {
|
||||
const completedBytes = Number(pullProgress.completed) || 0;
|
||||
const totalBytes = Number(pullProgress.total) || 0;
|
||||
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
|
||||
const safePercent = hasTotal
|
||||
? Math.min(
|
||||
100,
|
||||
Math.max(0, Math.round((completedBytes / totalBytes) * 100)),
|
||||
)
|
||||
: null;
|
||||
const percentText = hasTotal && safePercent !== null
|
||||
? `${safePercent.toFixed(0)}%`
|
||||
: pullProgress.status || t('处理中');
|
||||
{pullProgress &&
|
||||
(() => {
|
||||
const completedBytes = Number(pullProgress.completed) || 0;
|
||||
const totalBytes = Number(pullProgress.total) || 0;
|
||||
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
|
||||
const safePercent = hasTotal
|
||||
? Math.min(
|
||||
100,
|
||||
Math.max(
|
||||
0,
|
||||
Math.round((completedBytes / totalBytes) * 100),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
const percentText =
|
||||
hasTotal && safePercent !== null
|
||||
? `${safePercent.toFixed(0)}%`
|
||||
: pullProgress.status || t('处理中');
|
||||
|
||||
return (
|
||||
<div className='mt-3 p-3 bg-gray-50 rounded-lg'>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<Text strong>{t('拉取进度')}</Text>
|
||||
<Text type='tertiary' size='small'>{percentText}</Text>
|
||||
</div>
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<Text strong>{t('拉取进度')}</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{percentText}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{hasTotal && safePercent !== null ? (
|
||||
<div>
|
||||
<Progress
|
||||
percent={safePercent}
|
||||
showInfo={false}
|
||||
stroke='#1890ff'
|
||||
size='small'
|
||||
/>
|
||||
<div className='flex justify-between mt-1'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||
</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||
</Text>
|
||||
{hasTotal && safePercent !== null ? (
|
||||
<div>
|
||||
<Progress
|
||||
percent={safePercent}
|
||||
showInfo={false}
|
||||
stroke='#1890ff'
|
||||
size='small'
|
||||
/>
|
||||
<div className='flex justify-between mt-1'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)}{' '}
|
||||
GB
|
||||
</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
|
||||
<Spin size='small' />
|
||||
<span>{t('准备中...')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
) : (
|
||||
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
|
||||
<Spin size='small' />
|
||||
<span>{t('准备中...')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<Text type='tertiary' size='small' className='mt-2 block'>
|
||||
{t('支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间')}
|
||||
{t(
|
||||
'支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间',
|
||||
)}
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
{/* 已有模型列表 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<div className='flex items-center'>
|
||||
<Avatar size='small' color='purple' className='mr-2'>
|
||||
<IconServer size={16} />
|
||||
</Avatar>
|
||||
<Title heading={5} className='m-0'>
|
||||
<Card>
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Title heading={6} className='m-0'>
|
||||
{t('已有模型')}
|
||||
{models.length > 0 && (
|
||||
<Tag color='blue' className='ml-2'>
|
||||
{models.length}
|
||||
</Tag>
|
||||
)}
|
||||
</Title>
|
||||
{models.length > 0 ? (
|
||||
<Tag color='blue'>{models.length}</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Input
|
||||
@@ -688,7 +666,7 @@ const OllamaModelModal = ({
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
theme='borderless'
|
||||
theme='light'
|
||||
onClick={handleSelectAll}
|
||||
disabled={models.length === 0}
|
||||
>
|
||||
@@ -696,7 +674,7 @@ const OllamaModelModal = ({
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
theme='borderless'
|
||||
theme='light'
|
||||
onClick={handleClearSelection}
|
||||
disabled={selectedModelIds.length === 0}
|
||||
>
|
||||
@@ -728,11 +706,10 @@ const OllamaModelModal = ({
|
||||
<Spin spinning={loading}>
|
||||
{filteredModels.length === 0 ? (
|
||||
<Empty
|
||||
image={<IconServer size={60} />}
|
||||
title={searchValue ? t('未找到匹配的模型') : t('暂无模型')}
|
||||
description={
|
||||
searchValue
|
||||
? t('请尝试其他搜索关键词')
|
||||
searchValue
|
||||
? t('请尝试其他搜索关键词')
|
||||
: t('您可以在上方拉取需要的模型')
|
||||
}
|
||||
style={{ padding: '40px 0' }}
|
||||
@@ -740,25 +717,17 @@ const OllamaModelModal = ({
|
||||
) : (
|
||||
<List
|
||||
dataSource={filteredModels}
|
||||
split={false}
|
||||
renderItem={(model, index) => (
|
||||
<List.Item
|
||||
key={model.id}
|
||||
className='hover:bg-gray-50 rounded-lg p-3 transition-colors'
|
||||
>
|
||||
split
|
||||
renderItem={(model) => (
|
||||
<List.Item key={model.id}>
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<div className='flex items-center flex-1 min-w-0 gap-3'>
|
||||
<Checkbox
|
||||
checked={selectedModelIds.includes(model.id)}
|
||||
onChange={(checked) => handleToggleModel(model.id, checked)}
|
||||
onChange={(checked) =>
|
||||
handleToggleModel(model.id, checked)
|
||||
}
|
||||
/>
|
||||
<Avatar
|
||||
size='small'
|
||||
color='blue'
|
||||
className='flex-shrink-0'
|
||||
>
|
||||
{model.id.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<Text strong className='block truncate'>
|
||||
{model.id}
|
||||
@@ -775,10 +744,13 @@ const OllamaModelModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2 ml-4'>
|
||||
<div className='flex items-center space-x-2 ml-4'>
|
||||
<Popconfirm
|
||||
title={t('确认删除模型')}
|
||||
content={t('删除后无法恢复,确定要删除模型 "{{name}}" 吗?', { name: model.id })}
|
||||
content={t(
|
||||
'删除后无法恢复,确定要删除模型 "{{name}}" 吗?',
|
||||
{ name: model.id },
|
||||
)}
|
||||
onConfirm={() => deleteModel(model.id)}
|
||||
okText={t('确认')}
|
||||
cancelText={t('取消')}
|
||||
@@ -798,7 +770,7 @@ const OllamaModelModal = ({
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,13 +27,14 @@ const DeploymentsActions = ({
|
||||
setEditingDeployment,
|
||||
setShowEdit,
|
||||
batchDeleteDeployments,
|
||||
batchOperationsEnabled = true,
|
||||
compactMode,
|
||||
setCompactMode,
|
||||
showCreateModal,
|
||||
setShowCreateModal,
|
||||
t,
|
||||
}) => {
|
||||
const hasSelected = selectedKeys.length > 0;
|
||||
const hasSelected = batchOperationsEnabled && selectedKeys.length > 0;
|
||||
|
||||
const handleAddDeployment = () => {
|
||||
if (setShowCreateModal) {
|
||||
@@ -53,7 +54,6 @@ const DeploymentsActions = ({
|
||||
setSelectedKeys([]);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
|
||||
<Button
|
||||
|
||||
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
timestamp2string,
|
||||
showSuccess,
|
||||
showError,
|
||||
} from '../../../helpers';
|
||||
import { Button, Dropdown, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { timestamp2string, showSuccess, showError } from '../../../helpers';
|
||||
import { IconMore } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
FaPlay,
|
||||
@@ -50,7 +41,6 @@ import {
|
||||
FaHourglassHalf,
|
||||
FaGlobe,
|
||||
} from 'react-icons/fa';
|
||||
import {t} from "i18next";
|
||||
|
||||
const normalizeStatus = (status) =>
|
||||
typeof status === 'string' ? status.trim().toLowerCase() : '';
|
||||
@@ -58,59 +48,59 @@ const normalizeStatus = (status) =>
|
||||
const STATUS_TAG_CONFIG = {
|
||||
running: {
|
||||
color: 'green',
|
||||
label: t('运行中'),
|
||||
labelKey: '运行中',
|
||||
icon: <FaPlay size={12} className='text-green-600' />,
|
||||
},
|
||||
deploying: {
|
||||
color: 'blue',
|
||||
label: t('部署中'),
|
||||
labelKey: '部署中',
|
||||
icon: <FaSpinner size={12} className='text-blue-600' />,
|
||||
},
|
||||
pending: {
|
||||
color: 'orange',
|
||||
label: t('待部署'),
|
||||
labelKey: '待部署',
|
||||
icon: <FaClock size={12} className='text-orange-600' />,
|
||||
},
|
||||
stopped: {
|
||||
color: 'grey',
|
||||
label: t('已停止'),
|
||||
labelKey: '已停止',
|
||||
icon: <FaStop size={12} className='text-gray-500' />,
|
||||
},
|
||||
error: {
|
||||
color: 'red',
|
||||
label: t('错误'),
|
||||
labelKey: '错误',
|
||||
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
||||
},
|
||||
failed: {
|
||||
color: 'red',
|
||||
label: t('失败'),
|
||||
labelKey: '失败',
|
||||
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
||||
},
|
||||
destroyed: {
|
||||
color: 'red',
|
||||
label: t('已销毁'),
|
||||
labelKey: '已销毁',
|
||||
icon: <FaBan size={12} className='text-red-500' />,
|
||||
},
|
||||
completed: {
|
||||
color: 'green',
|
||||
label: t('已完成'),
|
||||
labelKey: '已完成',
|
||||
icon: <FaCheckCircle size={12} className='text-green-600' />,
|
||||
},
|
||||
'deployment requested': {
|
||||
color: 'blue',
|
||||
label: t('部署请求中'),
|
||||
labelKey: '部署请求中',
|
||||
icon: <FaSpinner size={12} className='text-blue-600' />,
|
||||
},
|
||||
'termination requested': {
|
||||
color: 'orange',
|
||||
label: t('终止请求中'),
|
||||
labelKey: '终止请求中',
|
||||
icon: <FaClock size={12} className='text-orange-600' />,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_STATUS_CONFIG = {
|
||||
color: 'grey',
|
||||
label: null,
|
||||
labelKey: null,
|
||||
icon: <FaInfoCircle size={12} className='text-gray-500' />,
|
||||
};
|
||||
|
||||
@@ -190,7 +180,9 @@ const renderStatus = (status, t) => {
|
||||
const normalizedStatus = normalizeStatus(status);
|
||||
const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
|
||||
const statusText = typeof status === 'string' ? status : '';
|
||||
const labelText = config.label ? t(config.label) : statusText || t('未知状态');
|
||||
const labelText = config.labelKey
|
||||
? t(config.labelKey)
|
||||
: statusText || t('未知状态');
|
||||
|
||||
return (
|
||||
<Tag
|
||||
@@ -206,20 +198,24 @@ const renderStatus = (status, t) => {
|
||||
|
||||
// Container Name Cell Component - to properly handle React hooks
|
||||
const ContainerNameCell = ({ text, record, t }) => {
|
||||
const handleCopyId = () => {
|
||||
navigator.clipboard.writeText(record.id);
|
||||
showSuccess(t('ID已复制到剪贴板'));
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(record.id);
|
||||
showSuccess(t('已复制 ID 到剪贴板'));
|
||||
} catch (err) {
|
||||
showError(t('复制失败'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Typography.Text strong className="text-base">
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography.Text strong className='text-base'>
|
||||
{text}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
size="small"
|
||||
className="text-xs cursor-pointer hover:text-blue-600 transition-colors select-all"
|
||||
<Typography.Text
|
||||
type='secondary'
|
||||
size='small'
|
||||
className='text-xs cursor-pointer hover:text-blue-600 transition-colors select-all'
|
||||
onClick={handleCopyId}
|
||||
title={t('点击复制ID')}
|
||||
>
|
||||
@@ -232,26 +228,26 @@ const ContainerNameCell = ({ text, record, t }) => {
|
||||
// Render resource configuration
|
||||
const renderResourceConfig = (resource, t) => {
|
||||
if (!resource) return '-';
|
||||
|
||||
|
||||
const { cpu, memory, gpu } = resource;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className='flex flex-col gap-1'>
|
||||
{cpu && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<FaMicrochip className="text-blue-500" />
|
||||
<div className='flex items-center gap-1 text-xs'>
|
||||
<FaMicrochip className='text-blue-500' />
|
||||
<span>CPU: {cpu}</span>
|
||||
</div>
|
||||
)}
|
||||
{memory && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<FaMemory className="text-green-500" />
|
||||
<div className='flex items-center gap-1 text-xs'>
|
||||
<FaMemory className='text-green-500' />
|
||||
<span>内存: {memory}</span>
|
||||
</div>
|
||||
)}
|
||||
{gpu && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<FaServer className="text-purple-500" />
|
||||
<div className='flex items-center gap-1 text-xs'>
|
||||
<FaServer className='text-purple-500' />
|
||||
<span>GPU: {gpu}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -266,7 +262,7 @@ const renderInstanceCount = (count, record, t) => {
|
||||
const countColor = statusConfig?.color ?? 'grey';
|
||||
|
||||
return (
|
||||
<Tag color={countColor} size="small" shape='circle'>
|
||||
<Tag color={countColor} size='small' shape='circle'>
|
||||
{count || 0} {t('个实例')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -299,11 +295,7 @@ export const getDeploymentsColumns = ({
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<ContainerNameCell
|
||||
text={text}
|
||||
record={record}
|
||||
t={t}
|
||||
/>
|
||||
<ContainerNameCell text={text} record={record} t={t} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -312,9 +304,7 @@ export const getDeploymentsColumns = ({
|
||||
key: COLUMN_KEYS.status,
|
||||
width: 140,
|
||||
render: (status) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{renderStatus(status, t)}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>{renderStatus(status, t)}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -325,18 +315,22 @@ export const getDeploymentsColumns = ({
|
||||
render: (provider) =>
|
||||
provider ? (
|
||||
<div
|
||||
className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide"
|
||||
className='flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide'
|
||||
style={{
|
||||
borderColor: 'rgba(59, 130, 246, 0.4)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: '#2563eb',
|
||||
}}
|
||||
>
|
||||
<FaGlobe className="text-[11px]" />
|
||||
<FaGlobe className='text-[11px]' />
|
||||
<span>{provider}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text type="tertiary" size="small" className="text-xs text-gray-500">
|
||||
<Typography.Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='text-xs text-gray-500'
|
||||
>
|
||||
{t('暂无')}
|
||||
</Typography.Text>
|
||||
),
|
||||
@@ -345,7 +339,7 @@ export const getDeploymentsColumns = ({
|
||||
title: t('剩余时间'),
|
||||
dataIndex: 'time_remaining',
|
||||
key: COLUMN_KEYS.time_remaining,
|
||||
width: 140,
|
||||
width: 200,
|
||||
render: (text, record) => {
|
||||
const normalizedStatus = normalizeStatus(record?.status);
|
||||
const percentUsedRaw = parsePercentValue(record?.completed_percent);
|
||||
@@ -380,43 +374,43 @@ export const getDeploymentsColumns = ({
|
||||
percentRemaining !== null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 leading-tight text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className='flex flex-col gap-1 leading-tight text-xs'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<FaHourglassHalf
|
||||
className="text-sm"
|
||||
className='text-sm'
|
||||
style={{ color: theme.iconColor }}
|
||||
/>
|
||||
<Typography.Text className="text-sm font-medium text-[var(--semi-color-text-0)]">
|
||||
<Typography.Text className='text-sm font-medium text-[var(--semi-color-text-0)]'>
|
||||
{timeDisplay}
|
||||
</Typography.Text>
|
||||
{showProgress && percentRemaining !== null ? (
|
||||
<Tag size="small" color={theme.tagColor}>
|
||||
<Tag size='small' color={theme.tagColor}>
|
||||
{percentRemaining}%
|
||||
</Tag>
|
||||
) : statusOverride ? (
|
||||
<Tag size="small" color="grey">
|
||||
<Tag size='small' color='grey'>
|
||||
{statusOverride}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
{showExtraInfo && (
|
||||
<div className="flex items-center gap-3 text-[var(--semi-color-text-2)]">
|
||||
<div className='flex items-center gap-3 text-[var(--semi-color-text-2)]'>
|
||||
{humanReadable && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FaClock className="text-[11px]" />
|
||||
<span className='flex items-center gap-1'>
|
||||
<FaClock className='text-[11px]' />
|
||||
{t('约')} {humanReadable}
|
||||
</span>
|
||||
)}
|
||||
{percentUsed !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FaCheckCircle className="text-[11px]" />
|
||||
<span className='flex items-center gap-1'>
|
||||
<FaCheckCircle className='text-[11px]' />
|
||||
{t('已用')} {percentUsed}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showProgress && showRemainingMeta && (
|
||||
<div className="text-[10px]" style={{ color: theme.textColor }}>
|
||||
<div className='text-[10px]' style={{ color: theme.textColor }}>
|
||||
{t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
|
||||
</div>
|
||||
)}
|
||||
@@ -431,14 +425,16 @@ export const getDeploymentsColumns = ({
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md">
|
||||
<FaServer className="text-green-600 text-xs" />
|
||||
<span className="text-xs font-medium text-green-700">
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md'>
|
||||
<FaServer className='text-green-600 text-xs' />
|
||||
<span className='text-xs font-medium text-green-700'>
|
||||
{record.hardware_name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 font-medium">x{record.hardware_quantity}</span>
|
||||
<span className='text-xs text-gray-500 font-medium'>
|
||||
x{record.hardware_quantity}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -448,7 +444,7 @@ export const getDeploymentsColumns = ({
|
||||
key: COLUMN_KEYS.created_at,
|
||||
width: 150,
|
||||
render: (text) => (
|
||||
<span className="text-sm text-gray-600">{timestamp2string(text)}</span>
|
||||
<span className='text-sm text-gray-600'>{timestamp2string(text)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -459,7 +455,8 @@ export const getDeploymentsColumns = ({
|
||||
render: (_, record) => {
|
||||
const { status, id } = record;
|
||||
const normalizedStatus = normalizeStatus(status);
|
||||
const isEnded = normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
|
||||
const isEnded =
|
||||
normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
|
||||
|
||||
const handleDelete = () => {
|
||||
// Use enhanced confirmation dialog
|
||||
@@ -471,7 +468,7 @@ export const getDeploymentsColumns = ({
|
||||
switch (normalizedStatus) {
|
||||
case 'running':
|
||||
return {
|
||||
icon: <FaInfoCircle className="text-xs" />,
|
||||
icon: <FaInfoCircle className='text-xs' />,
|
||||
text: t('查看详情'),
|
||||
onClick: () => onViewDetails?.(record),
|
||||
type: 'secondary',
|
||||
@@ -480,7 +477,7 @@ export const getDeploymentsColumns = ({
|
||||
case 'failed':
|
||||
case 'error':
|
||||
return {
|
||||
icon: <FaPlay className="text-xs" />,
|
||||
icon: <FaPlay className='text-xs' />,
|
||||
text: t('重试'),
|
||||
onClick: () => startDeployment(id),
|
||||
type: 'primary',
|
||||
@@ -488,7 +485,7 @@ export const getDeploymentsColumns = ({
|
||||
};
|
||||
case 'stopped':
|
||||
return {
|
||||
icon: <FaPlay className="text-xs" />,
|
||||
icon: <FaPlay className='text-xs' />,
|
||||
text: t('启动'),
|
||||
onClick: () => startDeployment(id),
|
||||
type: 'primary',
|
||||
@@ -497,7 +494,7 @@ export const getDeploymentsColumns = ({
|
||||
case 'deployment requested':
|
||||
case 'deploying':
|
||||
return {
|
||||
icon: <FaClock className="text-xs" />,
|
||||
icon: <FaClock className='text-xs' />,
|
||||
text: t('部署中'),
|
||||
onClick: () => {},
|
||||
type: 'secondary',
|
||||
@@ -506,7 +503,7 @@ export const getDeploymentsColumns = ({
|
||||
};
|
||||
case 'pending':
|
||||
return {
|
||||
icon: <FaClock className="text-xs" />,
|
||||
icon: <FaClock className='text-xs' />,
|
||||
text: t('待部署'),
|
||||
onClick: () => {},
|
||||
type: 'secondary',
|
||||
@@ -515,7 +512,7 @@ export const getDeploymentsColumns = ({
|
||||
};
|
||||
case 'termination requested':
|
||||
return {
|
||||
icon: <FaClock className="text-xs" />,
|
||||
icon: <FaClock className='text-xs' />,
|
||||
text: t('终止中'),
|
||||
onClick: () => {},
|
||||
type: 'secondary',
|
||||
@@ -526,7 +523,7 @@ export const getDeploymentsColumns = ({
|
||||
case 'destroyed':
|
||||
default:
|
||||
return {
|
||||
icon: <FaInfoCircle className="text-xs" />,
|
||||
icon: <FaInfoCircle className='text-xs' />,
|
||||
text: t('已结束'),
|
||||
onClick: () => {},
|
||||
type: 'tertiary',
|
||||
@@ -542,13 +539,13 @@ export const getDeploymentsColumns = ({
|
||||
|
||||
if (isEnded) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-start gap-1 pr-2">
|
||||
<div className='flex w-full items-center justify-start gap-1 pr-2'>
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
type='tertiary'
|
||||
theme='borderless'
|
||||
onClick={() => onViewDetails?.(record)}
|
||||
icon={<FaInfoCircle className="text-xs" />}
|
||||
icon={<FaInfoCircle className='text-xs' />}
|
||||
>
|
||||
{t('查看详情')}
|
||||
</Button>
|
||||
@@ -558,14 +555,22 @@ export const getDeploymentsColumns = ({
|
||||
|
||||
// All actions dropdown with enhanced operations
|
||||
const dropdownItems = [
|
||||
<Dropdown.Item key="details" onClick={() => onViewDetails?.(record)} icon={<FaInfoCircle />}>
|
||||
<Dropdown.Item
|
||||
key='details'
|
||||
onClick={() => onViewDetails?.(record)}
|
||||
icon={<FaInfoCircle />}
|
||||
>
|
||||
{t('查看详情')}
|
||||
</Dropdown.Item>,
|
||||
];
|
||||
|
||||
if (!isEnded) {
|
||||
dropdownItems.push(
|
||||
<Dropdown.Item key="logs" onClick={() => onViewLogs?.(record)} icon={<FaTerminal />}>
|
||||
<Dropdown.Item
|
||||
key='logs'
|
||||
onClick={() => onViewLogs?.(record)}
|
||||
icon={<FaTerminal />}
|
||||
>
|
||||
{t('查看日志')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
@@ -575,7 +580,11 @@ export const getDeploymentsColumns = ({
|
||||
if (normalizedStatus === 'running') {
|
||||
if (onSyncToChannel) {
|
||||
managementItems.push(
|
||||
<Dropdown.Item key="sync-channel" onClick={() => onSyncToChannel(record)} icon={<FaLink />}>
|
||||
<Dropdown.Item
|
||||
key='sync-channel'
|
||||
onClick={() => onSyncToChannel(record)}
|
||||
icon={<FaLink />}
|
||||
>
|
||||
{t('同步到渠道')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
@@ -583,28 +592,44 @@ export const getDeploymentsColumns = ({
|
||||
}
|
||||
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
|
||||
managementItems.push(
|
||||
<Dropdown.Item key="retry" onClick={() => startDeployment(id)} icon={<FaPlay />}>
|
||||
<Dropdown.Item
|
||||
key='retry'
|
||||
onClick={() => startDeployment(id)}
|
||||
icon={<FaPlay />}
|
||||
>
|
||||
{t('重试')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
}
|
||||
if (normalizedStatus === 'stopped') {
|
||||
managementItems.push(
|
||||
<Dropdown.Item key="start" onClick={() => startDeployment(id)} icon={<FaPlay />}>
|
||||
<Dropdown.Item
|
||||
key='start'
|
||||
onClick={() => startDeployment(id)}
|
||||
icon={<FaPlay />}
|
||||
>
|
||||
{t('启动')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
}
|
||||
|
||||
if (managementItems.length > 0) {
|
||||
dropdownItems.push(<Dropdown.Divider key="management-divider" />);
|
||||
dropdownItems.push(<Dropdown.Divider key='management-divider' />);
|
||||
dropdownItems.push(...managementItems);
|
||||
}
|
||||
|
||||
const configItems = [];
|
||||
if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) {
|
||||
if (
|
||||
!isEnded &&
|
||||
(normalizedStatus === 'running' ||
|
||||
normalizedStatus === 'deployment requested')
|
||||
) {
|
||||
configItems.push(
|
||||
<Dropdown.Item key="extend" onClick={() => onExtendDuration?.(record)} icon={<FaPlus />}>
|
||||
<Dropdown.Item
|
||||
key='extend'
|
||||
onClick={() => onExtendDuration?.(record)}
|
||||
icon={<FaPlus />}
|
||||
>
|
||||
{t('延长时长')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
@@ -618,13 +643,18 @@ export const getDeploymentsColumns = ({
|
||||
// }
|
||||
|
||||
if (configItems.length > 0) {
|
||||
dropdownItems.push(<Dropdown.Divider key="config-divider" />);
|
||||
dropdownItems.push(<Dropdown.Divider key='config-divider' />);
|
||||
dropdownItems.push(...configItems);
|
||||
}
|
||||
if (!isEnded) {
|
||||
dropdownItems.push(<Dropdown.Divider key="danger-divider" />);
|
||||
dropdownItems.push(<Dropdown.Divider key='danger-divider' />);
|
||||
dropdownItems.push(
|
||||
<Dropdown.Item key="delete" type="danger" onClick={handleDelete} icon={<FaTrash />}>
|
||||
<Dropdown.Item
|
||||
key='delete'
|
||||
type='danger'
|
||||
onClick={handleDelete}
|
||||
icon={<FaTrash />}
|
||||
>
|
||||
{t('销毁容器')}
|
||||
</Dropdown.Item>,
|
||||
);
|
||||
@@ -634,31 +664,31 @@ export const getDeploymentsColumns = ({
|
||||
const hasDropdown = dropdownItems.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-start gap-1 pr-2">
|
||||
<div className='flex w-full items-center justify-start gap-1 pr-2'>
|
||||
<Button
|
||||
size="small"
|
||||
size='small'
|
||||
theme={primaryTheme}
|
||||
type={primaryType}
|
||||
icon={primaryAction.icon}
|
||||
onClick={primaryAction.onClick}
|
||||
className="px-2 text-xs"
|
||||
className='px-2 text-xs'
|
||||
disabled={primaryAction.disabled}
|
||||
>
|
||||
{primaryAction.text}
|
||||
</Button>
|
||||
|
||||
|
||||
{hasDropdown && (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
render={allActions}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
size='small'
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconMore />}
|
||||
className="px-1"
|
||||
className='px-1'
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,8 @@ const DeploymentsTable = (deploymentsData) => {
|
||||
deploymentCount,
|
||||
compactMode,
|
||||
visibleColumns,
|
||||
setSelectedKeys,
|
||||
rowSelection,
|
||||
batchOperationsEnabled = true,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
handleRow,
|
||||
@@ -95,7 +96,10 @@ const DeploymentsTable = (deploymentsData) => {
|
||||
};
|
||||
|
||||
const handleConfirmAction = () => {
|
||||
if (selectedDeployment && confirmOperation === 'delete') {
|
||||
if (
|
||||
selectedDeployment &&
|
||||
(confirmOperation === 'delete' || confirmOperation === 'destroy')
|
||||
) {
|
||||
deleteDeployment(selectedDeployment.id);
|
||||
}
|
||||
setShowConfirmDialog(false);
|
||||
@@ -179,11 +183,7 @@ const DeploymentsTable = (deploymentsData) => {
|
||||
hidePagination={true}
|
||||
expandAllRows={false}
|
||||
onRow={handleRow}
|
||||
rowSelection={{
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
setSelectedKeys(selectedRows);
|
||||
},
|
||||
}}
|
||||
rowSelection={batchOperationsEnabled ? rowSelection : undefined}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
@@ -235,7 +235,7 @@ const DeploymentsTable = (deploymentsData) => {
|
||||
onCancel={() => setShowConfirmDialog(false)}
|
||||
onConfirm={handleConfirmAction}
|
||||
title={t('确认操作')}
|
||||
type="danger"
|
||||
type='danger'
|
||||
deployment={selectedDeployment}
|
||||
operation={confirmOperation}
|
||||
t={t}
|
||||
|
||||
@@ -32,9 +32,10 @@ import { createCardProPagination } from '../../../helpers/utils';
|
||||
const DeploymentsPage = () => {
|
||||
const deploymentsData = useDeploymentsData();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
|
||||
// Create deployment modal state
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const batchOperationsEnabled = false;
|
||||
|
||||
const {
|
||||
// Edit state
|
||||
@@ -81,7 +82,7 @@ const DeploymentsPage = () => {
|
||||
visible={showEdit}
|
||||
handleClose={closeEdit}
|
||||
/>
|
||||
|
||||
|
||||
<CreateDeploymentModal
|
||||
visible={showCreateModal}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
@@ -109,6 +110,7 @@ const DeploymentsPage = () => {
|
||||
setEditingDeployment={setEditingDeployment}
|
||||
setShowEdit={setShowEdit}
|
||||
batchDeleteDeployments={batchDeleteDeployments}
|
||||
batchOperationsEnabled={batchOperationsEnabled}
|
||||
compactMode={compactMode}
|
||||
setCompactMode={setCompactMode}
|
||||
showCreateModal={showCreateModal}
|
||||
@@ -138,7 +140,10 @@ const DeploymentsPage = () => {
|
||||
})}
|
||||
t={deploymentsData.t}
|
||||
>
|
||||
<DeploymentsTable {...deploymentsData} />
|
||||
<DeploymentsTable
|
||||
{...deploymentsData}
|
||||
batchOperationsEnabled={batchOperationsEnabled}
|
||||
/>
|
||||
</CardPro>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,27 +34,21 @@ import {
|
||||
TextArea,
|
||||
Switch,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
FaCog,
|
||||
import {
|
||||
FaCog,
|
||||
FaDocker,
|
||||
FaKey,
|
||||
FaTerminal,
|
||||
FaNetworkWired,
|
||||
FaExclamationTriangle,
|
||||
FaPlus,
|
||||
FaMinus
|
||||
FaMinus,
|
||||
} from 'react-icons/fa';
|
||||
import { API, showError, showSuccess } from '../../../../helpers';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const UpdateConfigModal = ({
|
||||
visible,
|
||||
onCancel,
|
||||
deployment,
|
||||
onSuccess,
|
||||
t
|
||||
}) => {
|
||||
const UpdateConfigModal = ({ visible, onCancel, deployment, onSuccess, t }) => {
|
||||
const formRef = useRef(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [envVars, setEnvVars] = useState([]);
|
||||
@@ -72,18 +66,21 @@ const UpdateConfigModal = ({
|
||||
registry_secret: '',
|
||||
command: '',
|
||||
};
|
||||
|
||||
|
||||
if (formRef.current) {
|
||||
formRef.current.setValues(initialValues);
|
||||
}
|
||||
|
||||
|
||||
// Initialize environment variables
|
||||
const envVarsList = deployment.container_config?.env_variables
|
||||
? Object.entries(deployment.container_config.env_variables).map(([key, value]) => ({
|
||||
key, value: String(value)
|
||||
}))
|
||||
const envVarsList = deployment.container_config?.env_variables
|
||||
? Object.entries(deployment.container_config.env_variables).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
value: String(value),
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
|
||||
setEnvVars(envVarsList);
|
||||
setSecretEnvVars([]);
|
||||
}
|
||||
@@ -91,23 +88,30 @@ const UpdateConfigModal = ({
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
const formValues = formRef.current ? await formRef.current.validate() : {};
|
||||
const formValues = formRef.current
|
||||
? await formRef.current.validate()
|
||||
: {};
|
||||
setLoading(true);
|
||||
|
||||
// Prepare the update payload
|
||||
const payload = {};
|
||||
|
||||
|
||||
if (formValues.image_url) payload.image_url = formValues.image_url;
|
||||
if (formValues.traffic_port) payload.traffic_port = formValues.traffic_port;
|
||||
if (formValues.registry_username) payload.registry_username = formValues.registry_username;
|
||||
if (formValues.registry_secret) payload.registry_secret = formValues.registry_secret;
|
||||
if (formValues.traffic_port)
|
||||
payload.traffic_port = formValues.traffic_port;
|
||||
if (formValues.registry_username)
|
||||
payload.registry_username = formValues.registry_username;
|
||||
if (formValues.registry_secret)
|
||||
payload.registry_secret = formValues.registry_secret;
|
||||
if (formValues.command) payload.command = formValues.command;
|
||||
|
||||
|
||||
// Process entrypoint
|
||||
if (formValues.entrypoint) {
|
||||
payload.entrypoint = formValues.entrypoint.split(' ').filter(cmd => cmd.trim());
|
||||
payload.entrypoint = formValues.entrypoint
|
||||
.split(' ')
|
||||
.filter((cmd) => cmd.trim());
|
||||
}
|
||||
|
||||
|
||||
// Process environment variables
|
||||
if (envVars.length > 0) {
|
||||
payload.env_variables = envVars.reduce((acc, env) => {
|
||||
@@ -117,7 +121,7 @@ const UpdateConfigModal = ({
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
// Process secret environment variables
|
||||
if (secretEnvVars.length > 0) {
|
||||
payload.secret_env_variables = secretEnvVars.reduce((acc, env) => {
|
||||
@@ -128,7 +132,10 @@ const UpdateConfigModal = ({
|
||||
}, {});
|
||||
}
|
||||
|
||||
const response = await API.put(`/api/deployments/${deployment.id}`, payload);
|
||||
const response = await API.put(
|
||||
`/api/deployments/${deployment.id}`,
|
||||
payload,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
showSuccess(t('容器配置更新成功'));
|
||||
@@ -136,7 +143,11 @@ const UpdateConfigModal = ({
|
||||
handleCancel();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
|
||||
showError(
|
||||
t('更新配置失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -184,8 +195,8 @@ const UpdateConfigModal = ({
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCog className="text-blue-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaCog className='text-blue-500' />
|
||||
<span>{t('更新容器配置')}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -196,130 +207,131 @@ const UpdateConfigModal = ({
|
||||
cancelText={t('取消')}
|
||||
confirmLoading={loading}
|
||||
width={700}
|
||||
className="update-config-modal"
|
||||
className='update-config-modal'
|
||||
>
|
||||
<div className="space-y-4 max-h-[600px] overflow-y-auto">
|
||||
<div className='space-y-4 max-h-[600px] overflow-y-auto'>
|
||||
{/* Container Info */}
|
||||
<Card className="border-0 bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className='border-0 bg-gray-50'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<Text strong className="text-base">
|
||||
<Text strong className='text-base'>
|
||||
{deployment?.container_name}
|
||||
</Text>
|
||||
<div className="mt-1">
|
||||
<Text type="secondary" size="small">
|
||||
<div className='mt-1'>
|
||||
<Text type='secondary' size='small'>
|
||||
ID: {deployment?.id}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Tag color="blue">{deployment?.status}</Tag>
|
||||
<Tag color='blue'>{deployment?.status}</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Warning Banner */}
|
||||
<Banner
|
||||
type="warning"
|
||||
type='warning'
|
||||
icon={<FaExclamationTriangle />}
|
||||
title={t('重要提醒')}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<p>{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}</p>
|
||||
<div className='space-y-2'>
|
||||
<p>
|
||||
{t(
|
||||
'更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。',
|
||||
)}
|
||||
</p>
|
||||
<p>{t('某些配置更改可能需要几分钟才能生效。')}</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form
|
||||
getFormApi={(api) => (formRef.current = api)}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form getFormApi={(api) => (formRef.current = api)} layout='vertical'>
|
||||
<Collapse defaultActiveKey={['docker']}>
|
||||
{/* Docker Configuration */}
|
||||
<Collapse.Panel
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaDocker className="text-blue-600" />
|
||||
<span>{t('Docker 配置')}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaDocker className='text-blue-600' />
|
||||
<span>{t('镜像配置')}</span>
|
||||
</div>
|
||||
}
|
||||
itemKey="docker"
|
||||
itemKey='docker'
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<Form.Input
|
||||
field="image_url"
|
||||
field='image_url'
|
||||
label={t('镜像地址')}
|
||||
placeholder={t('例如: nginx:latest')}
|
||||
rules={[
|
||||
{
|
||||
{
|
||||
type: 'string',
|
||||
message: t('请输入有效的镜像地址')
|
||||
}
|
||||
message: t('请输入有效的镜像地址'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="registry_username"
|
||||
field='registry_username'
|
||||
label={t('镜像仓库用户名')}
|
||||
placeholder={t('如果镜像为私有,请填写用户名')}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="registry_secret"
|
||||
field='registry_secret'
|
||||
label={t('镜像仓库密码')}
|
||||
mode="password"
|
||||
mode='password'
|
||||
placeholder={t('如果镜像为私有,请填写密码或Token')}
|
||||
/>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* Network Configuration */}
|
||||
<Collapse.Panel
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaNetworkWired className="text-green-600" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaNetworkWired className='text-green-600' />
|
||||
<span>{t('网络配置')}</span>
|
||||
</div>
|
||||
}
|
||||
itemKey="network"
|
||||
itemKey='network'
|
||||
>
|
||||
<Form.InputNumber
|
||||
field="traffic_port"
|
||||
field='traffic_port'
|
||||
label={t('流量端口')}
|
||||
placeholder={t('容器对外暴露的端口')}
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: '100%' }}
|
||||
rules={[
|
||||
{
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: t('端口号必须在1-65535之间')
|
||||
}
|
||||
message: t('端口号必须在1-65535之间'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* Startup Configuration */}
|
||||
<Collapse.Panel
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaTerminal className="text-purple-600" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaTerminal className='text-purple-600' />
|
||||
<span>{t('启动配置')}</span>
|
||||
</div>
|
||||
}
|
||||
itemKey="startup"
|
||||
itemKey='startup'
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<Form.Input
|
||||
field="entrypoint"
|
||||
field='entrypoint'
|
||||
label={t('启动命令 (Entrypoint)')}
|
||||
placeholder={t('例如: /bin/bash -c "python app.py"')}
|
||||
helpText={t('多个命令用空格分隔')}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="command"
|
||||
field='command'
|
||||
label={t('运行命令 (Command)')}
|
||||
placeholder={t('容器启动后执行的命令')}
|
||||
/>
|
||||
@@ -327,34 +339,34 @@ const UpdateConfigModal = ({
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<Collapse.Panel
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaKey className="text-orange-600" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaKey className='text-orange-600' />
|
||||
<span>{t('环境变量')}</span>
|
||||
<Tag size="small">{envVars.length}</Tag>
|
||||
<Tag size='small'>{envVars.length}</Tag>
|
||||
</div>
|
||||
}
|
||||
itemKey="env"
|
||||
itemKey='env'
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
{/* Regular Environment Variables */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<Text strong>{t('普通环境变量')}</Text>
|
||||
<Button
|
||||
size="small"
|
||||
size='small'
|
||||
icon={<FaPlus />}
|
||||
onClick={addEnvVar}
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
theme='borderless'
|
||||
type='primary'
|
||||
>
|
||||
{t('添加')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{envVars.map((envVar, index) => (
|
||||
<div key={index} className="flex items-end gap-2 mb-2">
|
||||
<div key={index} className='flex items-end gap-2 mb-2'>
|
||||
<Input
|
||||
placeholder={t('变量名')}
|
||||
value={envVar.key}
|
||||
@@ -365,22 +377,24 @@ const UpdateConfigModal = ({
|
||||
<Input
|
||||
placeholder={t('变量值')}
|
||||
value={envVar.value}
|
||||
onChange={(value) => updateEnvVar(index, 'value', value)}
|
||||
onChange={(value) =>
|
||||
updateEnvVar(index, 'value', value)
|
||||
}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
size='small'
|
||||
icon={<FaMinus />}
|
||||
onClick={() => removeEnvVar(index)}
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
theme='borderless'
|
||||
type='danger'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
{envVars.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg">
|
||||
<Text type="secondary">{t('暂无环境变量')}</Text>
|
||||
<div className='text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg'>
|
||||
<Text type='secondary'>{t('暂无环境变量')}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -389,61 +403,67 @@ const UpdateConfigModal = ({
|
||||
|
||||
{/* Secret Environment Variables */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Text strong>{t('机密环境变量')}</Text>
|
||||
<Tag size="small" type="danger">
|
||||
<Tag size='small' type='danger'>
|
||||
{t('加密存储')}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
size='small'
|
||||
icon={<FaPlus />}
|
||||
onClick={addSecretEnvVar}
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
theme='borderless'
|
||||
type='danger'
|
||||
>
|
||||
{t('添加')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{secretEnvVars.map((envVar, index) => (
|
||||
<div key={index} className="flex items-end gap-2 mb-2">
|
||||
<div key={index} className='flex items-end gap-2 mb-2'>
|
||||
<Input
|
||||
placeholder={t('变量名')}
|
||||
value={envVar.key}
|
||||
onChange={(value) => updateSecretEnvVar(index, 'key', value)}
|
||||
onChange={(value) =>
|
||||
updateSecretEnvVar(index, 'key', value)
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Text>=</Text>
|
||||
<Input
|
||||
mode="password"
|
||||
mode='password'
|
||||
placeholder={t('变量值')}
|
||||
value={envVar.value}
|
||||
onChange={(value) => updateSecretEnvVar(index, 'value', value)}
|
||||
onChange={(value) =>
|
||||
updateSecretEnvVar(index, 'value', value)
|
||||
}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
size='small'
|
||||
icon={<FaMinus />}
|
||||
onClick={() => removeSecretEnvVar(index)}
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
theme='borderless'
|
||||
type='danger'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
{secretEnvVars.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50">
|
||||
<Text type="secondary">{t('暂无机密环境变量')}</Text>
|
||||
<div className='text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50'>
|
||||
<Text type='secondary'>{t('暂无机密环境变量')}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<Banner
|
||||
type="info"
|
||||
type='info'
|
||||
title={t('机密环境变量说明')}
|
||||
description={t('机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。')}
|
||||
size="small"
|
||||
description={t(
|
||||
'机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。',
|
||||
)}
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,16 +472,18 @@ const UpdateConfigModal = ({
|
||||
</Form>
|
||||
|
||||
{/* Final Warning */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<FaExclamationTriangle className="text-yellow-600 mt-0.5" />
|
||||
<div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
|
||||
<div className='flex items-start gap-2'>
|
||||
<FaExclamationTriangle className='text-yellow-600 mt-0.5' />
|
||||
<div>
|
||||
<Text strong className="text-yellow-800">
|
||||
<Text strong className='text-yellow-800'>
|
||||
{t('配置更新确认')}
|
||||
</Text>
|
||||
<div className="mt-1">
|
||||
<Text size="small" className="text-yellow-700">
|
||||
{t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')}
|
||||
<div className='mt-1'>
|
||||
<Text size='small' className='text-yellow-700'>
|
||||
{t(
|
||||
'更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,4 +494,4 @@ const UpdateConfigModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateConfigModal;
|
||||
export default UpdateConfigModal;
|
||||
|
||||
@@ -31,8 +31,8 @@ import {
|
||||
Badge,
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
FaInfoCircle,
|
||||
import {
|
||||
FaInfoCircle,
|
||||
FaServer,
|
||||
FaClock,
|
||||
FaMapMarkerAlt,
|
||||
@@ -43,16 +43,16 @@ import {
|
||||
FaLink,
|
||||
} from 'react-icons/fa';
|
||||
import { IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../../../../helpers';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const ViewDetailsModal = ({
|
||||
visible,
|
||||
onCancel,
|
||||
deployment,
|
||||
t
|
||||
}) => {
|
||||
const ViewDetailsModal = ({ visible, onCancel, deployment, t }) => {
|
||||
const [details, setDetails] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [containers, setContainers] = useState([]);
|
||||
@@ -60,7 +60,7 @@ const ViewDetailsModal = ({
|
||||
|
||||
const fetchDetails = async () => {
|
||||
if (!deployment?.id) return;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await API.get(`/api/deployments/${deployment.id}`);
|
||||
@@ -68,7 +68,11 @@ const ViewDetailsModal = ({
|
||||
setDetails(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
|
||||
showError(
|
||||
t('获取详情失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -79,12 +83,18 @@ const ViewDetailsModal = ({
|
||||
|
||||
setContainersLoading(true);
|
||||
try {
|
||||
const response = await API.get(`/api/deployments/${deployment.id}/containers`);
|
||||
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));
|
||||
showError(
|
||||
t('获取容器信息失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setContainersLoading(false);
|
||||
}
|
||||
@@ -102,7 +112,7 @@ const ViewDetailsModal = ({
|
||||
|
||||
const handleCopyId = () => {
|
||||
navigator.clipboard.writeText(deployment?.id);
|
||||
showSuccess(t('ID已复制到剪贴板'));
|
||||
showSuccess(t('已复制 ID 到剪贴板'));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
@@ -112,12 +122,16 @@ const ViewDetailsModal = ({
|
||||
|
||||
const getStatusConfig = (status) => {
|
||||
const statusConfig = {
|
||||
'running': { color: 'green', text: '运行中', icon: '🟢' },
|
||||
'completed': { color: 'green', text: '已完成', icon: '✅' },
|
||||
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: '❌' }
|
||||
'termination requested': {
|
||||
color: 'orange',
|
||||
text: '终止请求中',
|
||||
icon: '⏸️',
|
||||
},
|
||||
destroyed: { color: 'red', text: '已销毁', icon: '🔴' },
|
||||
failed: { color: 'red', text: '失败', icon: '❌' },
|
||||
};
|
||||
return statusConfig[status] || { color: 'grey', text: status, icon: '❓' };
|
||||
};
|
||||
@@ -127,149 +141,167 @@ const ViewDetailsModal = ({
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaInfoCircle className="text-blue-500" />
|
||||
<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 />}
|
||||
<div className='flex justify-between'>
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading || containersLoading}
|
||||
theme="borderless"
|
||||
theme='borderless'
|
||||
>
|
||||
{t('刷新')}
|
||||
</Button>
|
||||
<Button onClick={onCancel}>
|
||||
{t('关闭')}
|
||||
</Button>
|
||||
<Button onClick={onCancel}>{t('关闭')}</Button>
|
||||
</div>
|
||||
}
|
||||
width={800}
|
||||
className="deployment-details-modal"
|
||||
className='deployment-details-modal'
|
||||
>
|
||||
{loading && !details ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spin size="large" tip={t('加载详情中...')} />
|
||||
<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">
|
||||
<div className='space-y-4 max-h-[600px] overflow-y-auto'>
|
||||
{/* Basic Info */}
|
||||
<Card
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaServer className="text-blue-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaServer className='text-blue-500' />
|
||||
<span>{t('基本信息')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
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}
|
||||
<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>
|
||||
<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)
|
||||
}
|
||||
]} />
|
||||
),
|
||||
},
|
||||
{
|
||||
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
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaChartLine className="text-green-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaChartLine className='text-green-500' />
|
||||
<span>{t('硬件与性能')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
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>
|
||||
)
|
||||
}
|
||||
]} />
|
||||
<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">
|
||||
<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'}
|
||||
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 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>
|
||||
@@ -277,56 +309,70 @@ const ViewDetailsModal = ({
|
||||
|
||||
{/* Container Configuration */}
|
||||
{details.container_config && (
|
||||
<Card
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaDocker className="text-blue-600" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaDocker className='text-blue-600' />
|
||||
<span>{t('容器配置')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
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>
|
||||
)
|
||||
}
|
||||
]} />
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
@@ -334,50 +380,63 @@ const ViewDetailsModal = ({
|
||||
{/* Containers List */}
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaServer className="text-indigo-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaServer className='text-indigo-500' />
|
||||
<span>{t('容器实例')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
className='border-0 shadow-sm'
|
||||
>
|
||||
{containersLoading ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className='flex items-center justify-center py-6'>
|
||||
<Spin tip={t('加载容器信息中...')} />
|
||||
</div>
|
||||
) : containers.length === 0 ? (
|
||||
<Empty description={t('暂无容器信息')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description={t('暂无容器信息')}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className='space-y-3'>
|
||||
{containers.map((ctr) => (
|
||||
<Card
|
||||
key={ctr.container_id}
|
||||
className="bg-gray-50 border border-gray-100"
|
||||
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">
|
||||
<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 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 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">
|
||||
<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')}
|
||||
size='small'
|
||||
theme='light'
|
||||
onClick={() =>
|
||||
window.open(
|
||||
ctr.public_url,
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('访问容器')}
|
||||
</Button>
|
||||
@@ -387,17 +446,26 @@ const ViewDetailsModal = ({
|
||||
</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">
|
||||
<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">
|
||||
<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) : '--'}
|
||||
<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">
|
||||
<span className='text-gray-700 break-all flex-1'>
|
||||
{event.message || '--'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -413,21 +481,23 @@ const ViewDetailsModal = ({
|
||||
|
||||
{/* Location Information */}
|
||||
{details.locations && details.locations.length > 0 && (
|
||||
<Card
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaMapMarkerAlt className="text-orange-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaMapMarkerAlt className='text-orange-500' />
|
||||
<span>{t('部署位置')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
className='border-0 shadow-sm'
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<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">
|
||||
<Tag key={location.id} color='orange' size='large'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<span>🌍</span>
|
||||
<span>{location.name} ({location.iso2})</span>
|
||||
<span>
|
||||
{location.name} ({location.iso2})
|
||||
</span>
|
||||
</div>
|
||||
</Tag>
|
||||
))}
|
||||
@@ -436,68 +506,82 @@ const ViewDetailsModal = ({
|
||||
)}
|
||||
|
||||
{/* Cost Information */}
|
||||
<Card
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaMoneyBillWave className="text-green-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaMoneyBillWave className='text-green-500' />
|
||||
<span>{t('费用信息')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
className='border-0 shadow-sm'
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
||||
<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 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 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 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
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaClock className="text-purple-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaClock className='text-purple-500' />
|
||||
<span>{t('时间信息')}</span>
|
||||
</div>
|
||||
}
|
||||
className="border-0 shadow-sm"
|
||||
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>
|
||||
<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
|
||||
{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
|
||||
<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>
|
||||
<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>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Text type='secondary'>{t('最后更新')}:</Text>
|
||||
<Text>{timestamp2string(details.updated_at)}</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,7 +589,7 @@ const ViewDetailsModal = ({
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<Empty
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={t('无法获取容器详情')}
|
||||
/>
|
||||
|
||||
@@ -44,18 +44,19 @@ import {
|
||||
FaLink,
|
||||
} from 'react-icons/fa';
|
||||
import { IconRefresh, IconDownload } from '@douyinfe/semi-icons';
|
||||
import { API, showError, showSuccess, copy, timestamp2string } from '../../../../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
copy,
|
||||
timestamp2string,
|
||||
} from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ALL_CONTAINERS = '__all__';
|
||||
|
||||
const ViewLogsModal = ({
|
||||
visible,
|
||||
onCancel,
|
||||
deployment,
|
||||
t
|
||||
}) => {
|
||||
const ViewLogsModal = ({ visible, onCancel, deployment, t }) => {
|
||||
const [logLines, setLogLines] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||
@@ -63,12 +64,13 @@ const ViewLogsModal = ({
|
||||
const [following, setFollowing] = useState(false);
|
||||
const [containers, setContainers] = useState([]);
|
||||
const [containersLoading, setContainersLoading] = useState(false);
|
||||
const [selectedContainerId, setSelectedContainerId] = useState(ALL_CONTAINERS);
|
||||
const [selectedContainerId, setSelectedContainerId] =
|
||||
useState(ALL_CONTAINERS);
|
||||
const [containerDetails, setContainerDetails] = useState(null);
|
||||
const [containerDetailsLoading, setContainerDetailsLoading] = useState(false);
|
||||
const [streamFilter, setStreamFilter] = useState('stdout');
|
||||
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
|
||||
|
||||
|
||||
const logContainerRef = useRef(null);
|
||||
const autoRefreshRef = useRef(null);
|
||||
|
||||
@@ -100,7 +102,10 @@ const ViewLogsModal = ({
|
||||
const fetchLogs = async (containerIdOverride = undefined) => {
|
||||
if (!deployment?.id) return;
|
||||
|
||||
const containerId = typeof containerIdOverride === 'string' ? containerIdOverride : selectedContainerId;
|
||||
const containerId =
|
||||
typeof containerIdOverride === 'string'
|
||||
? containerIdOverride
|
||||
: selectedContainerId;
|
||||
|
||||
if (!containerId || containerId === ALL_CONTAINERS) {
|
||||
setLogLines([]);
|
||||
@@ -120,10 +125,13 @@ const ViewLogsModal = ({
|
||||
}
|
||||
if (following) params.append('follow', 'true');
|
||||
|
||||
const response = await API.get(`/api/deployments/${deployment.id}/logs?${params}`);
|
||||
const response = await API.get(
|
||||
`/api/deployments/${deployment.id}/logs?${params}`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const rawContent = typeof response.data.data === 'string' ? response.data.data : '';
|
||||
const rawContent =
|
||||
typeof response.data.data === 'string' ? response.data.data : '';
|
||||
const normalized = rawContent.replace(/\r\n?/g, '\n');
|
||||
const lines = normalized ? normalized.split('\n') : [];
|
||||
|
||||
@@ -133,7 +141,11 @@ const ViewLogsModal = ({
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
|
||||
showError(
|
||||
t('获取日志失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -144,14 +156,19 @@ const ViewLogsModal = ({
|
||||
|
||||
setContainersLoading(true);
|
||||
try {
|
||||
const response = await API.get(`/api/deployments/${deployment.id}/containers`);
|
||||
const response = await API.get(
|
||||
`/api/deployments/${deployment.id}/containers`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const list = response.data.data?.containers || [];
|
||||
setContainers(list);
|
||||
|
||||
setSelectedContainerId((current) => {
|
||||
if (current !== ALL_CONTAINERS && list.some(item => item.container_id === current)) {
|
||||
if (
|
||||
current !== ALL_CONTAINERS &&
|
||||
list.some((item) => item.container_id === current)
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -163,7 +180,11 @@ const ViewLogsModal = ({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取容器列表失败') + ': ' + (error.response?.data?.message || error.message));
|
||||
showError(
|
||||
t('获取容器列表失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setContainersLoading(false);
|
||||
}
|
||||
@@ -177,13 +198,19 @@ const ViewLogsModal = ({
|
||||
|
||||
setContainerDetailsLoading(true);
|
||||
try {
|
||||
const response = await API.get(`/api/deployments/${deployment.id}/containers/${containerId}`);
|
||||
const response = await API.get(
|
||||
`/api/deployments/${deployment.id}/containers/${containerId}`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
setContainerDetails(response.data.data || null);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取容器详情失败') + ': ' + (error.response?.data?.message || error.message));
|
||||
showError(
|
||||
t('获取容器详情失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
} finally {
|
||||
setContainerDetailsLoading(false);
|
||||
}
|
||||
@@ -205,13 +232,14 @@ const ViewLogsModal = ({
|
||||
const renderContainerStatusTag = (status) => {
|
||||
if (!status) {
|
||||
return (
|
||||
<Tag color="grey" size="small">
|
||||
<Tag color='grey' size='small'>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : '';
|
||||
const normalized =
|
||||
typeof status === 'string' ? status.trim().toLowerCase() : '';
|
||||
const statusMap = {
|
||||
running: { color: 'green', label: '运行中' },
|
||||
pending: { color: 'orange', label: '准备中' },
|
||||
@@ -225,15 +253,16 @@ const ViewLogsModal = ({
|
||||
const config = statusMap[normalized] || { color: 'grey', label: status };
|
||||
|
||||
return (
|
||||
<Tag color={config.color} size="small">
|
||||
<Tag color={config.color} size='small'>
|
||||
{t(config.label)}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const currentContainer = selectedContainerId !== ALL_CONTAINERS
|
||||
? containers.find((ctr) => ctr.container_id === selectedContainerId)
|
||||
: null;
|
||||
const currentContainer =
|
||||
selectedContainerId !== ALL_CONTAINERS
|
||||
? containers.find((ctr) => ctr.container_id === selectedContainerId)
|
||||
: null;
|
||||
|
||||
const refreshLogs = () => {
|
||||
if (selectedContainerId && selectedContainerId !== ALL_CONTAINERS) {
|
||||
@@ -254,9 +283,10 @@ const ViewLogsModal = ({
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const safeContainerId = selectedContainerId && selectedContainerId !== ALL_CONTAINERS
|
||||
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
|
||||
: '';
|
||||
const safeContainerId =
|
||||
selectedContainerId && selectedContainerId !== ALL_CONTAINERS
|
||||
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
|
||||
: '';
|
||||
const fileName = safeContainerId
|
||||
? `deployment-${deployment.id}-container-${safeContainerId}-logs.txt`
|
||||
: `deployment-${deployment.id}-logs.txt`;
|
||||
@@ -265,7 +295,7 @@ const ViewLogsModal = ({
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
|
||||
showSuccess(t('日志已下载'));
|
||||
};
|
||||
|
||||
@@ -346,14 +376,15 @@ const ViewLogsModal = ({
|
||||
// Filter logs based on search term
|
||||
const filteredLogs = logLines
|
||||
.map((line) => line ?? '')
|
||||
.filter((line) =>
|
||||
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
.filter(
|
||||
(line) =>
|
||||
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const renderLogEntry = (line, index) => (
|
||||
<div
|
||||
key={`${index}-${line.slice(0, 20)}`}
|
||||
className="py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words"
|
||||
className='py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words'
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
@@ -362,10 +393,10 @@ const ViewLogsModal = ({
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FaTerminal className="text-blue-500" />
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaTerminal className='text-blue-500' />
|
||||
<span>{t('容器日志')}</span>
|
||||
<Text type="secondary" size="small">
|
||||
<Text type='secondary' size='small'>
|
||||
- {deployment?.container_name || deployment?.id}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -375,13 +406,13 @@ const ViewLogsModal = ({
|
||||
footer={null}
|
||||
width={1000}
|
||||
height={700}
|
||||
className="logs-modal"
|
||||
className='logs-modal'
|
||||
style={{ top: 20 }}
|
||||
>
|
||||
<div className="flex flex-col h-full max-h-[600px]">
|
||||
<div className='flex flex-col h-full max-h-[600px]'>
|
||||
{/* Controls */}
|
||||
<Card className="mb-4 border-0 shadow-sm">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<Card className='mb-4 border-0 shadow-sm'>
|
||||
<div className='flex items-center justify-between flex-wrap gap-3'>
|
||||
<Space wrap>
|
||||
<Select
|
||||
prefix={<FaServer />}
|
||||
@@ -389,7 +420,7 @@ const ViewLogsModal = ({
|
||||
value={selectedContainerId}
|
||||
onChange={handleContainerChange}
|
||||
style={{ width: 240 }}
|
||||
size="small"
|
||||
size='small'
|
||||
loading={containersLoading}
|
||||
dropdownStyle={{ maxHeight: 320, overflowY: 'auto' }}
|
||||
>
|
||||
@@ -397,10 +428,15 @@ const ViewLogsModal = ({
|
||||
{t('全部容器')}
|
||||
</Select.Option>
|
||||
{containers.map((ctr) => (
|
||||
<Select.Option key={ctr.container_id} value={ctr.container_id}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-xs">{ctr.container_id}</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
<Select.Option
|
||||
key={ctr.container_id}
|
||||
value={ctr.container_id}
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-mono text-xs'>
|
||||
{ctr.container_id}
|
||||
</span>
|
||||
<span className='text-xs text-gray-500'>
|
||||
{ctr.brand_name || 'IO.NET'}
|
||||
{ctr.hardware ? ` · ${ctr.hardware}` : ''}
|
||||
</span>
|
||||
@@ -415,114 +451,118 @@ const ViewLogsModal = ({
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
style={{ width: 200 }}
|
||||
size="small"
|
||||
size='small'
|
||||
/>
|
||||
|
||||
<Space align="center" className="ml-2">
|
||||
<Text size="small" type="secondary">
|
||||
|
||||
<Space align='center' className='ml-2'>
|
||||
<Text size='small' type='secondary'>
|
||||
{t('日志流')}
|
||||
</Text>
|
||||
<Radio.Group
|
||||
type="button"
|
||||
size="small"
|
||||
type='button'
|
||||
size='small'
|
||||
value={streamFilter}
|
||||
onChange={handleStreamChange}
|
||||
>
|
||||
<Radio value="stdout">STDOUT</Radio>
|
||||
<Radio value="stderr">STDERR</Radio>
|
||||
<Radio value='stdout'>STDOUT</Radio>
|
||||
<Radio value='stderr'>STDERR</Radio>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Switch
|
||||
checked={autoRefresh}
|
||||
onChange={setAutoRefresh}
|
||||
size="small"
|
||||
size='small'
|
||||
/>
|
||||
<Text size="small">{t('自动刷新')}</Text>
|
||||
<Text size='small'>{t('自动刷新')}</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Switch
|
||||
checked={following}
|
||||
onChange={setFollowing}
|
||||
size="small"
|
||||
size='small'
|
||||
/>
|
||||
<Text size="small">{t('跟随日志')}</Text>
|
||||
<Text size='small'>{t('跟随日志')}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Tooltip content={t('刷新日志')}>
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={refreshLogs}
|
||||
loading={loading}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
<Tooltip content={t('复制日志')}>
|
||||
<Button
|
||||
icon={<FaCopy />}
|
||||
<Button
|
||||
icon={<FaCopy />}
|
||||
onClick={copyAllLogs}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
disabled={logLines.length === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
<Tooltip content={t('下载日志')}>
|
||||
<Button
|
||||
icon={<IconDownload />}
|
||||
<Button
|
||||
icon={<IconDownload />}
|
||||
onClick={downloadLogs}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
disabled={logLines.length === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Status Info */}
|
||||
<Divider margin="12px" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Space size="large">
|
||||
<Text size="small" type="secondary">
|
||||
<Divider margin='12px' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<Space size='large'>
|
||||
<Text size='small' type='secondary'>
|
||||
{t('共 {{count}} 条日志', { count: logLines.length })}
|
||||
</Text>
|
||||
{searchTerm && (
|
||||
<Text size="small" type="secondary">
|
||||
{t('(筛选后显示 {{count}} 条)', { count: filteredLogs.length })}
|
||||
<Text size='small' type='secondary'>
|
||||
{t('(筛选后显示 {{count}} 条)', {
|
||||
count: filteredLogs.length,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
{autoRefresh && (
|
||||
<Tag color="green" size="small">
|
||||
<FaClock className="mr-1" />
|
||||
<Tag color='green' size='small'>
|
||||
<FaClock className='mr-1' />
|
||||
{t('自动刷新中')}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Text size="small" type="secondary">
|
||||
|
||||
<Text size='small' type='secondary'>
|
||||
{t('状态')}: {deployment?.status || 'unknown'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{selectedContainerId !== ALL_CONTAINERS && (
|
||||
<>
|
||||
<Divider margin="12px" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<Divider margin='12px' />
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex items-center justify-between flex-wrap gap-2'>
|
||||
<Space>
|
||||
<Tag color="blue" size="small">
|
||||
<Tag color='blue' size='small'>
|
||||
{t('容器')}
|
||||
</Tag>
|
||||
<Text className="font-mono text-xs">
|
||||
<Text className='font-mono text-xs'>
|
||||
{selectedContainerId}
|
||||
</Text>
|
||||
{renderContainerStatusTag(containerDetails?.status || currentContainer?.status)}
|
||||
{renderContainerStatusTag(
|
||||
containerDetails?.status || currentContainer?.status,
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
@@ -530,9 +570,11 @@ const ViewLogsModal = ({
|
||||
<Tooltip content={containerDetails.public_url}>
|
||||
<Button
|
||||
icon={<FaLink />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={() => window.open(containerDetails.public_url, '_blank')}
|
||||
size='small'
|
||||
theme='borderless'
|
||||
onClick={() =>
|
||||
window.open(containerDetails.public_url, '_blank')
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -540,8 +582,8 @@ const ViewLogsModal = ({
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={refreshContainerDetails}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
size='small'
|
||||
theme='borderless'
|
||||
loading={containerDetailsLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -549,27 +591,36 @@ const ViewLogsModal = ({
|
||||
</div>
|
||||
|
||||
{containerDetailsLoading ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className='flex items-center justify-center py-6'>
|
||||
<Spin tip={t('加载容器详情中...')} />
|
||||
</div>
|
||||
) : containerDetails ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaInfoCircle className="text-blue-500" />
|
||||
<Text type="secondary">{t('硬件')}</Text>
|
||||
<div className='grid gap-4 md:grid-cols-2 text-sm'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaInfoCircle className='text-blue-500' />
|
||||
<Text type='secondary'>{t('硬件')}</Text>
|
||||
<Text>
|
||||
{containerDetails?.brand_name || currentContainer?.brand_name || t('未知品牌')}
|
||||
{(containerDetails?.hardware || currentContainer?.hardware) ? ` · ${containerDetails?.hardware || currentContainer?.hardware}` : ''}
|
||||
{containerDetails?.brand_name ||
|
||||
currentContainer?.brand_name ||
|
||||
t('未知品牌')}
|
||||
{containerDetails?.hardware ||
|
||||
currentContainer?.hardware
|
||||
? ` · ${containerDetails?.hardware || currentContainer?.hardware}`
|
||||
: ''}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaServer className="text-purple-500" />
|
||||
<Text type="secondary">{t('GPU/容器')}</Text>
|
||||
<Text>{containerDetails?.gpus_per_container ?? currentContainer?.gpus_per_container ?? 0}</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaServer className='text-purple-500' />
|
||||
<Text type='secondary'>{t('GPU/容器')}</Text>
|
||||
<Text>
|
||||
{containerDetails?.gpus_per_container ??
|
||||
currentContainer?.gpus_per_container ??
|
||||
0}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaClock className="text-orange-500" />
|
||||
<Text type="secondary">{t('创建时间')}</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaClock className='text-orange-500' />
|
||||
<Text type='secondary'>{t('创建时间')}</Text>
|
||||
<Text>
|
||||
{containerDetails?.created_at
|
||||
? timestamp2string(containerDetails.created_at)
|
||||
@@ -578,51 +629,64 @@ const ViewLogsModal = ({
|
||||
: t('未知')}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaInfoCircle className="text-green-500" />
|
||||
<Text type="secondary">{t('运行时长')}</Text>
|
||||
<Text>{containerDetails?.uptime_percent ?? currentContainer?.uptime_percent ?? 0}%</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FaInfoCircle className='text-green-500' />
|
||||
<Text type='secondary'>{t('运行时长')}</Text>
|
||||
<Text>
|
||||
{containerDetails?.uptime_percent ??
|
||||
currentContainer?.uptime_percent ??
|
||||
0}
|
||||
%
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" type="secondary">
|
||||
<Text size='small' type='secondary'>
|
||||
{t('暂无容器详情')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{containerDetails?.events && containerDetails.events.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<Text size="small" type="secondary">
|
||||
{t('最近事件')}
|
||||
</Text>
|
||||
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto">
|
||||
{containerDetails.events.slice(0, 5).map((event, index) => (
|
||||
<div key={`${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
|
||||
<span className="text-gray-500">
|
||||
{event.time ? timestamp2string(event.time) : '--'}
|
||||
</span>
|
||||
<span className="text-gray-700 break-all flex-1">
|
||||
{event.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{containerDetails?.events &&
|
||||
containerDetails.events.length > 0 && (
|
||||
<div className='bg-gray-50 rounded-lg p-3'>
|
||||
<Text size='small' type='secondary'>
|
||||
{t('最近事件')}
|
||||
</Text>
|
||||
<div className='mt-2 space-y-2 max-h-32 overflow-y-auto'>
|
||||
{containerDetails.events
|
||||
.slice(0, 5)
|
||||
.map((event, index) => (
|
||||
<div
|
||||
key={`${event.time}-${index}`}
|
||||
className='flex gap-3 text-xs font-mono'
|
||||
>
|
||||
<span className='text-gray-500'>
|
||||
{event.time
|
||||
? timestamp2string(event.time)
|
||||
: '--'}
|
||||
</span>
|
||||
<span className='text-gray-700 break-all flex-1'>
|
||||
{event.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Log Content */}
|
||||
<div className="flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden">
|
||||
<div
|
||||
<div className='flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden'>
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
className="flex-1 overflow-y-auto bg-white"
|
||||
className='flex-1 overflow-y-auto bg-white'
|
||||
style={{ maxHeight: '400px' }}
|
||||
>
|
||||
{loading && logLines.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className='flex items-center justify-center p-8'>
|
||||
<Spin tip={t('加载日志中...')} />
|
||||
</div>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
@@ -639,15 +703,14 @@ const ViewLogsModal = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Footer status */}
|
||||
{logLines.length > 0 && (
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500">
|
||||
<div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500'>
|
||||
<span>{following ? t('正在跟随最新日志') : t('日志已加载')}</span>
|
||||
<span>
|
||||
{following ? t('正在跟随最新日志') : t('日志已加载')}
|
||||
</span>
|
||||
<span>
|
||||
{t('最后更新')}: {lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
|
||||
{t('最后更新')}:{' '}
|
||||
{lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user