From 22d0b73d21537b7a4625e4c4e9ca591830dbb49f Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Sat, 3 Jan 2026 12:37:50 +0800 Subject: [PATCH] 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. --- controller/deployment.go | 37 +- controller/option.go | 6 +- router/api-router.go | 18 +- web/i18next.config.js | 4 +- .../DeploymentAccessGuard.jsx | 251 ++- .../personal/cards/NotificationSettings.jsx | 44 +- .../channels/modals/OllamaModelModal.jsx | 250 +-- .../model-deployments/DeploymentsActions.jsx | 4 +- .../DeploymentsColumnDefs.jsx | 244 ++- .../model-deployments/DeploymentsTable.jsx | 16 +- .../table/model-deployments/index.jsx | 11 +- .../modals/CreateDeploymentModal.jsx | 987 +++++---- .../modals/UpdateConfigModal.jsx | 264 +-- .../modals/ViewDetailsModal.jsx | 544 +++-- .../modals/ViewLogsModal.jsx | 343 +-- web/src/hooks/common/useSidebar.js | 87 +- .../useDeploymentResources.js | 154 +- .../model-deployments/useDeploymentsData.jsx | 209 +- .../useEnhancedDeploymentActions.jsx | 141 +- .../useModelDeploymentSettings.js | 44 +- web/src/i18n/locales/en.json | 401 +++- web/src/i18n/locales/fr.json | 439 +++- web/src/i18n/locales/ja.json | 540 ++++- web/src/i18n/locales/ru.json | 445 +++- web/src/i18n/locales/vi.json | 1922 ++++++++++------- web/src/i18n/locales/zh.json | 437 +++- web/src/pages/ModelDeployment/index.jsx | 3 +- .../Setting/Model/SettingModelDeployment.jsx | 22 +- .../Personal/SettingsSidebarModulesUser.jsx | 44 +- 29 files changed, 5258 insertions(+), 2653 deletions(-) diff --git a/controller/deployment.go b/controller/deployment.go index 7530b4edf..a2ffedc66 100644 --- a/controller/deployment.go +++ b/controller/deployment.go @@ -1,6 +1,8 @@ package controller import ( + "bytes" + "encoding/json" "fmt" "strconv" "strings" @@ -23,6 +25,20 @@ func getIoAPIKey(c *gin.Context) (string, bool) { return apiKey, true } +func GetModelDeploymentSettings(c *gin.Context) { + common.OptionMapRWMutex.RLock() + enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true" + hasAPIKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"]) != "" + common.OptionMapRWMutex.RUnlock() + + common.ApiSuccess(c, gin.H{ + "provider": "io.net", + "enabled": enabled, + "configured": hasAPIKey, + "can_connect": enabled && hasAPIKey, + }) +} + func getIoClient(c *gin.Context) (*ionet.Client, bool) { apiKey, ok := getIoAPIKey(c) if !ok { @@ -44,15 +60,28 @@ func TestIoNetConnection(c *gin.Context) { APIKey string `json:"api_key"` } - if err := c.ShouldBindJSON(&req); err != nil { - common.ApiErrorMsg(c, "invalid request payload") + rawBody, err := c.GetRawData() + if err != nil { + common.ApiError(c, err) return } + if len(bytes.TrimSpace(rawBody)) > 0 { + if err := json.Unmarshal(rawBody, &req); err != nil { + common.ApiErrorMsg(c, "invalid request payload") + return + } + } apiKey := strings.TrimSpace(req.APIKey) if apiKey == "" { - common.ApiErrorMsg(c, "api_key is required") - return + common.OptionMapRWMutex.RLock() + storedKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"]) + common.OptionMapRWMutex.RUnlock() + if storedKey == "" { + common.ApiErrorMsg(c, "api_key is required") + return + } + apiKey = storedKey } client := ionet.NewEnterpriseClient(apiKey) diff --git a/controller/option.go b/controller/option.go index 89b2fc4d5..4d5b4e8d2 100644 --- a/controller/option.go +++ b/controller/option.go @@ -20,7 +20,11 @@ func GetOptions(c *gin.Context) { var options []*model.Option common.OptionMapRWMutex.Lock() for k, v := range common.OptionMap { - if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") || strings.HasSuffix(k, "Key") { + if strings.HasSuffix(k, "Token") || + strings.HasSuffix(k, "Secret") || + strings.HasSuffix(k, "Key") || + strings.HasSuffix(k, "secret") || + strings.HasSuffix(k, "api_key") { continue } options = append(options, &model.Option{ diff --git a/router/api-router.go b/router/api-router.go index 800c5c657..9b2bd0615 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -269,24 +269,18 @@ func SetApiRouter(router *gin.Engine) { deploymentsRoute := apiRouter.Group("/deployments") deploymentsRoute.Use(middleware.AdminAuth()) { - // List and search deployments + deploymentsRoute.GET("/settings", controller.GetModelDeploymentSettings) + deploymentsRoute.POST("/settings/test-connection", controller.TestIoNetConnection) deploymentsRoute.GET("/", controller.GetAllDeployments) deploymentsRoute.GET("/search", controller.SearchDeployments) - - // Connection utilities deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection) - - // Resource and configuration endpoints deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes) deploymentsRoute.GET("/locations", controller.GetLocations) deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas) deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation) deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability) - - // Create new deployment deploymentsRoute.POST("/", controller.CreateDeployment) - // Individual deployment operations deploymentsRoute.GET("/:id", controller.GetDeployment) deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs) deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers) @@ -295,14 +289,6 @@ func SetApiRouter(router *gin.Engine) { deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName) deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment) deploymentsRoute.DELETE("/:id", controller.DeleteDeployment) - - // Future batch operations: - // deploymentsRoute.POST("/:id/start", controller.StartDeployment) - // deploymentsRoute.POST("/:id/stop", controller.StopDeployment) - // deploymentsRoute.POST("/:id/restart", controller.RestartDeployment) - // deploymentsRoute.POST("/batch_delete", controller.BatchDeleteDeployments) - // deploymentsRoute.POST("/batch_start", controller.BatchStartDeployments) - // deploymentsRoute.POST("/batch_stop", controller.BatchStopDeployments) } } } diff --git a/web/i18next.config.js b/web/i18next.config.js index ca6b4a5f3..4e138bdfc 100644 --- a/web/i18next.config.js +++ b/web/i18next.config.js @@ -25,7 +25,9 @@ export default defineConfig({ "zh", "en", "fr", - "ru" + "ru", + "ja", + "vi" ], extract: { input: [ diff --git a/web/src/components/model-deployments/DeploymentAccessGuard.jsx b/web/src/components/model-deployments/DeploymentAccessGuard.jsx index f771fa1c5..eef17b364 100644 --- a/web/src/components/model-deployments/DeploymentAccessGuard.jsx +++ b/web/src/components/model-deployments/DeploymentAccessGuard.jsx @@ -46,7 +46,7 @@ const DeploymentAccessGuard = ({
- {t('加载设置中...')} + {t('加载设置中...')}
@@ -55,21 +55,21 @@ const DeploymentAccessGuard = ({ if (!isEnabled) { return ( -
-
{/* 图标区域 */}
-
- +
+
{/* 标题区域 */}
- {t('模型部署服务未启用')} - {t('访问模型部署功能需要先启用 io.net 部署服务')} @@ -124,75 +128,99 @@ const DeploymentAccessGuard = ({
{/* 配置要求区域 */} -
-
-
- + gap: '12px', + marginBottom: '16px', + }} + > +
+
- {t('需要配置的项目')}
- -
-
-
- + +
+
+
+ {t('启用 io.net 部署开关')}
-
-
- +
+
+ {t('配置有效的 io.net API Key')}
@@ -201,9 +229,9 @@ const DeploymentAccessGuard = ({ {/* 操作链接区域 */}
-
{ - e.target.style.background = 'var(--semi-color-fill-1)'; - e.target.style.transform = 'translateY(-1px)'; - e.target.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; + e.currentTarget.style.background = 'var(--semi-color-fill-1)'; + e.currentTarget.style.transform = 'translateY(-1px)'; + e.currentTarget.style.boxShadow = + '0 2px 8px rgba(0, 0, 0, 0.1)'; }} onMouseLeave={(e) => { - e.target.style.background = 'var(--semi-color-fill-0)'; - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = 'none'; + e.currentTarget.style.background = 'var(--semi-color-fill-0)'; + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = 'none'; }} > @@ -235,12 +264,12 @@ const DeploymentAccessGuard = ({
{/* 底部提示 */} - {t('配置完成后刷新页面即可使用模型部署功能')} @@ -256,7 +285,7 @@ const DeploymentAccessGuard = ({
- {t('Checking io.net connection...')} + {t('正在检查 io.net 连接...')}
@@ -265,12 +294,10 @@ const DeploymentAccessGuard = ({ if (connectionOk === false) { const isExpired = connectionError?.type === 'expired'; - const title = isExpired - ? t('API key expired') - : t('io.net connection unavailable'); + const title = isExpired ? t('接口密钥已过期') : t('无法连接 io.net'); const description = isExpired - ? t('The current API key is expired. Please update it in settings.') - : t('Unable to connect to io.net with the current configuration.'); + ? t('当前 API 密钥已过期,请在设置中更新。') + : t('当前配置无法连接到 io.net。'); const detail = connectionError?.message || ''; return ( @@ -297,7 +324,8 @@ const DeploymentAccessGuard = ({ borderRadius: '16px', border: '1px solid var(--semi-color-border)', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', - background: 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)', + background: + 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)', }} >
@@ -309,12 +337,13 @@ const DeploymentAccessGuard = ({ width: '120px', height: '120px', borderRadius: '50%', - background: 'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)', + background: + 'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)', border: '3px solid rgba(var(--semi-red-4), 0.3)', marginBottom: '24px', }} > - +
@@ -342,7 +371,7 @@ const DeploymentAccessGuard = ({
{detail ? ( -
- {onRetry ? ( - ) : null}
diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index c19084a51..0c51d239f 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -44,7 +44,10 @@ import CodeViewer from '../../../playground/CodeViewer'; import { StatusContext } from '../../../../context/Status'; import { UserContext } from '../../../../context/User'; import { useUserPermissions } from '../../../../hooks/common/useUserPermissions'; -import { useSidebar } from '../../../../hooks/common/useSidebar'; +import { + mergeAdminConfig, + useSidebar, +} from '../../../../hooks/common/useSidebar'; const NotificationSettings = ({ t, @@ -82,6 +85,7 @@ const NotificationSettings = ({ enabled: true, channel: true, models: true, + deployment: true, redemption: true, user: true, setting: true, @@ -164,6 +168,7 @@ const NotificationSettings = ({ enabled: true, channel: true, models: true, + deployment: true, redemption: true, user: true, setting: true, @@ -178,14 +183,27 @@ const NotificationSettings = ({ try { // 获取管理员全局配置 if (statusState?.status?.SidebarModulesAdmin) { - const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); - setAdminConfig(adminConf); + try { + const adminConf = JSON.parse( + statusState.status.SidebarModulesAdmin, + ); + setAdminConfig(mergeAdminConfig(adminConf)); + } catch (error) { + setAdminConfig(mergeAdminConfig(null)); + } + } else { + setAdminConfig(mergeAdminConfig(null)); } // 获取用户个人配置 const userRes = await API.get('/api/user/self'); if (userRes.data.success && userRes.data.data.sidebar_modules) { - const userConf = JSON.parse(userRes.data.data.sidebar_modules); + let userConf; + if (typeof userRes.data.data.sidebar_modules === 'string') { + userConf = JSON.parse(userRes.data.data.sidebar_modules); + } else { + userConf = userRes.data.data.sidebar_modules; + } setSidebarModulesUser(userConf); } } catch (error) { @@ -273,6 +291,11 @@ const NotificationSettings = ({ modules: [ { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, { key: 'models', title: t('模型管理'), description: t('AI模型配置') }, + { + key: 'deployment', + title: t('模型部署'), + description: t('模型部署管理'), + }, { key: 'redemption', title: t('兑换码管理'), @@ -812,7 +835,9 @@ const NotificationSettings = ({
@@ -835,7 +860,8 @@ const NotificationSettings = ({ >
diff --git a/web/src/components/table/channels/modals/OllamaModelModal.jsx b/web/src/components/table/channels/modals/OllamaModelModal.jsx index 8b1dfcce1..684d2eb46 100644 --- a/web/src/components/table/channels/modals/OllamaModelModal.jsx +++ b/web/src/components/table/channels/modals/OllamaModelModal.jsx @@ -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 ( - - - -
- - {t('Ollama 模型管理')} - - - {channelInfo?.name && `${channelInfo.name} - `} - {t('管理 Ollama 模型的拉取和删除')} - -
-
- } + title={t('Ollama 模型管理')} visible={visible} onCancel={onCancel} - width={800} + width={720} style={{ maxWidth: '95vw' }} footer={ -
- -
+ } > -
+ +
+ + {channelInfo?.name ? `${channelInfo.name} - ` : ''} + {t('管理 Ollama 模型的拉取和删除')} + +
+ {/* 拉取新模型 */} - -
- - - - - {t('拉取新模型')} - -
- + + + {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('处理中'); + {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 ( -
-
- {t('拉取进度')} - {percentText} -
+ return ( +
+
+ {t('拉取进度')} + + {percentText} + +
- {hasTotal && safePercent !== null ? ( -
- -
- - {(completedBytes / (1024 * 1024 * 1024)).toFixed(2)} GB - - - {(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB - + {hasTotal && safePercent !== null ? ( +
+ +
+ + {(completedBytes / (1024 * 1024 * 1024)).toFixed(2)}{' '} + GB + + + {(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB + +
-
- ) : ( -
- - {t('准备中...')} -
- )} -
- ); - })()} - + ) : ( +
+ + {t('准备中...')} +
+ )} +
+ ); + })()} + - {t('支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间')} + {t( + '支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间', + )} {/* 已有模型列表 */} - -
-
- - - - + <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> - )} + {models.length > 0 ? ( + {models.length} + ) : null}
@@ -558,14 +555,22 @@ export const getDeploymentsColumns = ({ // All actions dropdown with enhanced operations const dropdownItems = [ - onViewDetails?.(record)} icon={}> + onViewDetails?.(record)} + icon={} + > {t('查看详情')} , ]; if (!isEnded) { dropdownItems.push( - onViewLogs?.(record)} icon={}> + onViewLogs?.(record)} + icon={} + > {t('查看日志')} , ); @@ -575,7 +580,11 @@ export const getDeploymentsColumns = ({ if (normalizedStatus === 'running') { if (onSyncToChannel) { managementItems.push( - onSyncToChannel(record)} icon={}> + onSyncToChannel(record)} + icon={} + > {t('同步到渠道')} , ); @@ -583,28 +592,44 @@ export const getDeploymentsColumns = ({ } if (normalizedStatus === 'failed' || normalizedStatus === 'error') { managementItems.push( - startDeployment(id)} icon={}> + startDeployment(id)} + icon={} + > {t('重试')} , ); } if (normalizedStatus === 'stopped') { managementItems.push( - startDeployment(id)} icon={}> + startDeployment(id)} + icon={} + > {t('启动')} , ); } if (managementItems.length > 0) { - dropdownItems.push(); + dropdownItems.push(); dropdownItems.push(...managementItems); } const configItems = []; - if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) { + if ( + !isEnded && + (normalizedStatus === 'running' || + normalizedStatus === 'deployment requested') + ) { configItems.push( - onExtendDuration?.(record)} icon={}> + onExtendDuration?.(record)} + icon={} + > {t('延长时长')} , ); @@ -618,13 +643,18 @@ export const getDeploymentsColumns = ({ // } if (configItems.length > 0) { - dropdownItems.push(); + dropdownItems.push(); dropdownItems.push(...configItems); } if (!isEnded) { - dropdownItems.push(); + dropdownItems.push(); dropdownItems.push( - }> + } + > {t('销毁容器')} , ); @@ -634,31 +664,31 @@ export const getDeploymentsColumns = ({ const hasDropdown = dropdownItems.length > 0; return ( -
+
- + {hasDropdown && (
- ))} - -
+ + + -
- {t('启动参数 (Args)')} - {args.map((arg, index) => ( -
- handleArrayFieldChange(index, value, 'args')} - style={{ flex: 1, marginRight: 8 }} - /> -
- ))} - -
- + - + + {t('容器启动配置')} - - {t('环境变量')} - -
- {t('普通环境变量')} - {envVariables.map((env, index) => ( - - +
+ {t('启动命令 (Entrypoint)')} + {entrypoint.map((cmd, index) => ( +
handleEnvVariableChange(index, 'key', value, 'env')} + value={cmd} + placeholder={t('例如:/bin/bash')} + onChange={(value) => + handleArrayFieldChange(index, value, 'entrypoint') + } + style={{ flex: 1, marginRight: 8 }} /> - - - handleEnvVariableChange(index, 'value', value, 'env')} - /> - - -
+
+ ))} + +
-
- {t('密钥环境变量')} - {secretEnvVariables.map((env, index) => { - const isAutoSecret = - imageMode === 'builtin' && env.key === 'OLLAMA_API_KEY'; - return ( +
+ {t('启动参数 (Args)')} + {args.map((arg, index) => ( +
+ + handleArrayFieldChange(index, value, 'args') + } + style={{ flex: 1, marginRight: 8 }} + /> +
+ ))} + +
+ + + + + + {t('环境变量')} + +
+ {t('普通环境变量')} + {envVariables.map((env, index) => ( handleEnvVariableChange(index, 'key', value, 'secret')} - disabled={isAutoSecret} + onChange={(value) => + handleEnvVariableChange( + index, + 'key', + value, + 'env', + ) + } /> handleEnvVariableChange(index, 'value', value, 'secret')} - disabled={isAutoSecret} + onChange={(value) => + handleEnvVariableChange( + index, + 'value', + value, + 'env', + ) + } /> -
-
- - -
-
-
- -
- -
- - {t('价格预估')} - - - - {t('计价币种')} - - - USDC - IOCOIN - - - {currencyLabel} - - -
- - {priceEstimation ? ( -
-
-
- - {t('预估总费用')} - -
} + onClick={() => handleAddEnvVariable('env')} + style={{ marginTop: 8 }} > - {typeof priceEstimation.estimated_cost === 'number' - ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}` - : '--'} -
+ {t('添加环境变量')} +
-
- - {t('小时费率')} - - - {typeof priceEstimation.price_breakdown?.hourly_rate === 'number' - ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h` - : '--'} - -
-
- - {t('计算成本')} - - - {typeof priceEstimation.price_breakdown?.compute_cost === 'number' - ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}` - : '--'} - -
-
-
- {priceSummaryItems.map((item) => ( -
+ {t('密钥环境变量')} + {secretEnvVariables.map((env, index) => { + const isAutoSecret = + imageMode === 'builtin' && + env.key === 'OLLAMA_API_KEY'; + return ( + + + + handleEnvVariableChange( + index, + 'key', + value, + 'secret', + ) + } + disabled={isAutoSecret} + /> + + + + handleEnvVariableChange( + index, + 'value', + value, + 'secret', + ) + } + disabled={isAutoSecret} + /> + + +
- ))} -
-
- ) : ( - priceUnavailableContent - )} - - {priceEstimation && loadingPrice && ( - - - - {t('价格重新计算中...')} - - - )} + {t('添加密钥环境变量')} + +
+
+ + +
+
+ +
+ + {t('价格预估')} + + + + {t('计价币种')} + + + USDC + IOCOIN + + + {currencyLabel} + + +
+ + {priceEstimation ? ( +
+
+
+ + {t('预估总费用')} + +
+ {typeof priceEstimation.estimated_cost === 'number' + ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}` + : '--'} +
+
+
+ + {t('小时费率')} + + + {typeof priceEstimation.price_breakdown?.hourly_rate === + 'number' + ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h` + : '--'} + +
+
+ + {t('计算成本')} + + + {typeof priceEstimation.price_breakdown?.compute_cost === + 'number' + ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}` + : '--'} + +
+
+ +
+ {priceSummaryItems.map((item) => ( +
+ + {item.label} + + {item.value} +
+ ))} +
+
+ ) : ( + priceUnavailableContent + )} + + {priceEstimation && loadingPrice && ( + + + + {t('价格重新计算中...')} + + + )} +
+
); diff --git a/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx b/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx index 3b21b8b68..8d30415db 100644 --- a/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx +++ b/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx @@ -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 ( - +
+ {t('更新容器配置')}
} @@ -196,130 +207,131 @@ const UpdateConfigModal = ({ cancelText={t('取消')} confirmLoading={loading} width={700} - className="update-config-modal" + className='update-config-modal' > -
+
{/* Container Info */} - -
+ +
- + {deployment?.container_name} -
- +
+ ID: {deployment?.id}
- {deployment?.status} + {deployment?.status}
{/* Warning Banner */} } title={t('重要提醒')} description={ -
-

{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}

+
+

+ {t( + '更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。', + )} +

{t('某些配置更改可能需要几分钟才能生效。')}

} /> -
(formRef.current = api)} - layout="vertical" - > + (formRef.current = api)} layout='vertical'> {/* Docker Configuration */} - - - {t('Docker 配置')} +
+ + {t('镜像配置')}
} - itemKey="docker" + itemKey='docker' > -
+
{/* Network Configuration */} - - +
+ {t('网络配置')}
} - itemKey="network" + itemKey='network' >
{/* Startup Configuration */} - - +
+ {t('启动配置')}
} - itemKey="startup" + itemKey='startup' > -
+
@@ -327,34 +339,34 @@ const UpdateConfigModal = ({ {/* Environment Variables */} - - +
+ {t('环境变量')} - {envVars.length} + {envVars.length}
} - itemKey="env" + itemKey='env' > -
+
{/* Regular Environment Variables */}
-
+
{t('普通环境变量')}
- + {envVars.map((envVar, index) => ( -
+
updateEnvVar(index, 'value', value)} + onChange={(value) => + updateEnvVar(index, 'value', value) + } style={{ flex: 2 }} />
))} - + {envVars.length === 0 && ( -
- {t('暂无环境变量')} +
+ {t('暂无环境变量')}
)}
@@ -389,61 +403,67 @@ const UpdateConfigModal = ({ {/* Secret Environment Variables */}
-
-
+
+
{t('机密环境变量')} - + {t('加密存储')}
- + {secretEnvVars.map((envVar, index) => ( -
+
updateSecretEnvVar(index, 'key', value)} + onChange={(value) => + updateSecretEnvVar(index, 'key', value) + } style={{ flex: 1 }} /> = updateSecretEnvVar(index, 'value', value)} + onChange={(value) => + updateSecretEnvVar(index, 'value', value) + } style={{ flex: 2 }} />
))} - + {secretEnvVars.length === 0 && ( -
- {t('暂无机密环境变量')} +
+ {t('暂无机密环境变量')}
)} - +
@@ -452,16 +472,18 @@ const UpdateConfigModal = ({ {/* Final Warning */} -
-
- +
+
+
- + {t('配置更新确认')} -
- - {t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')} +
+ + {t( + '更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。', + )}
@@ -472,4 +494,4 @@ const UpdateConfigModal = ({ ); }; -export default UpdateConfigModal; \ No newline at end of file +export default UpdateConfigModal; diff --git a/web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx b/web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx index 7967e96e5..f004fe54e 100644 --- a/web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx +++ b/web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx @@ -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 ( - +
+ {t('容器详情')}
} visible={visible} onCancel={onCancel} footer={ -
- - +
} width={800} - className="deployment-details-modal" + className='deployment-details-modal' > {loading && !details ? ( -
- +
+
) : details ? ( -
+
{/* Basic Info */} - - +
+ {t('基本信息')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > - - - {details.deployment_name || details.id} + + + {details.deployment_name || details.id} + +
+ ), + }, + { + key: t('容器ID'), + value: ( + + {details.id} -
- ) - }, - { - key: t('容器ID'), - value: ( - - {details.id} - - ) - }, - { - key: t('状态'), - value: ( -
- {statusConfig.icon} - - {t(statusConfig.text)} - -
- ) - }, - { - key: t('创建时间'), - value: timestamp2string(details.created_at) - } - ]} /> + ), + }, + { + key: t('状态'), + value: ( +
+ {statusConfig.icon} + + {t(statusConfig.text)} + +
+ ), + }, + { + key: t('创建时间'), + value: timestamp2string(details.created_at), + }, + ]} + /> {/* Hardware & Performance */} - - +
+ {t('硬件与性能')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > -
- - {details.brand_name} - {details.hardware_name} -
- ) - }, - { - key: t('GPU数量'), - value: ( -
- - - - {t('总计')} {details.total_gpus} {t('个GPU')} -
- ) - }, - { - key: t('容器配置'), - value: ( -
-
{t('每容器GPU数')}: {details.gpus_per_container}
-
{t('容器总数')}: {details.total_containers}
-
- ) - } - ]} /> +
+ + {details.brand_name} + {details.hardware_name} +
+ ), + }, + { + key: t('GPU数量'), + value: ( +
+ + + + + {t('总计')} {details.total_gpus} {t('个GPU')} + +
+ ), + }, + { + key: t('容器配置'), + value: ( +
+
+ {t('每容器GPU数')}: {details.gpus_per_container} +
+
+ {t('容器总数')}: {details.total_containers} +
+
+ ), + }, + ]} + /> {/* Progress Bar */} -
-
+
+
{t('完成进度')} {details.completed_percent}%
-
- {t('已服务')}: {details.compute_minutes_served} {t('分钟')} - {t('剩余')}: {details.compute_minutes_remaining} {t('分钟')} +
+ + {t('已服务')}: {details.compute_minutes_served} {t('分钟')} + + + {t('剩余')}: {details.compute_minutes_remaining} {t('分钟')} +
@@ -277,56 +309,70 @@ const ViewDetailsModal = ({ {/* Container Configuration */} {details.container_config && ( - - +
+ {t('容器配置')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > -
- - {details.container_config.image_url || 'N/A'} - - ) - }, - { - key: t('流量端口'), - value: details.container_config.traffic_port || 'N/A' - }, - { - key: t('启动命令'), - value: ( - - {details.container_config.entrypoint ? - details.container_config.entrypoint.join(' ') : 'N/A' - } - - ) - } - ]} /> +
+ + {details.container_config.image_url || 'N/A'} + + ), + }, + { + key: t('流量端口'), + value: details.container_config.traffic_port || 'N/A', + }, + { + key: t('启动命令'), + value: ( + + {details.container_config.entrypoint + ? details.container_config.entrypoint.join(' ') + : 'N/A'} + + ), + }, + ]} + /> {/* Environment Variables */} - {details.container_config.env_variables && - Object.keys(details.container_config.env_variables).length > 0 && ( -
- {t('环境变量')}: -
- {Object.entries(details.container_config.env_variables).map(([key, value]) => ( -
- {key}= - {String(value)} -
- ))} + {details.container_config.env_variables && + Object.keys(details.container_config.env_variables).length > + 0 && ( +
+ + {t('环境变量')}: + +
+ {Object.entries( + details.container_config.env_variables, + ).map(([key, value]) => ( +
+ + {key}= + + + {String(value)} + +
+ ))} +
-
- )} + )}
)} @@ -334,50 +380,63 @@ const ViewDetailsModal = ({ {/* Containers List */} - +
+ {t('容器实例')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > {containersLoading ? ( -
+
) : containers.length === 0 ? ( - + ) : ( -
+
{containers.map((ctr) => ( -
-
- +
+
+ {ctr.container_id} - - {t('设备')} {ctr.device_id || '--'} · {t('状态')} {ctr.status || '--'} + + {t('设备')} {ctr.device_id || '--'} · {t('状态')}{' '} + {ctr.status || '--'} - - {t('创建时间')}: {ctr.created_at ? timestamp2string(ctr.created_at) : '--'} + + {t('创建时间')}:{' '} + {ctr.created_at + ? timestamp2string(ctr.created_at) + : '--'}
-
- +
+ {t('GPU/容器')}: {ctr.gpus_per_container ?? '--'} {ctr.public_url && ( @@ -387,17 +446,26 @@ const ViewDetailsModal = ({
{ctr.events && ctr.events.length > 0 && ( -
- +
+ {t('最近事件')} -
+
{ctr.events.map((event, index) => ( -
- - {event.time ? timestamp2string(event.time) : '--'} +
+ + {event.time + ? timestamp2string(event.time) + : '--'} - + {event.message || '--'}
@@ -413,21 +481,23 @@ const ViewDetailsModal = ({ {/* Location Information */} {details.locations && details.locations.length > 0 && ( - - +
+ {t('部署位置')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > -
+
{details.locations.map((location) => ( - -
+ +
🌍 - {location.name} ({location.iso2}) + + {location.name} ({location.iso2}) +
))} @@ -436,68 +506,82 @@ const ViewDetailsModal = ({ )} {/* Cost Information */} - - +
+ {t('费用信息')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > -
-
+
+
{t('已支付金额')} - - ${details.amount_paid ? details.amount_paid.toFixed(2) : '0.00'} USDC + + $ + {details.amount_paid + ? details.amount_paid.toFixed(2) + : '0.00'}{' '} + USDC
- -
-
- {t('计费开始')}: - {details.started_at ? timestamp2string(details.started_at) : 'N/A'} + +
+
+ {t('计费开始')}: + + {details.started_at + ? timestamp2string(details.started_at) + : 'N/A'} +
-
- {t('预计结束')}: - {details.finished_at ? timestamp2string(details.finished_at) : 'N/A'} +
+ {t('预计结束')}: + + {details.finished_at + ? timestamp2string(details.finished_at) + : 'N/A'} +
{/* Time Information */} - - +
+ {t('时间信息')}
} - className="border-0 shadow-sm" + className='border-0 shadow-sm' > -
-
-
- {t('已运行时间')}: +
+
+
+ {t('已运行时间')}: - {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
-
- {t('剩余时间')}: - - {Math.floor(details.compute_minutes_remaining / 60)}h {details.compute_minutes_remaining % 60}m +
+ {t('剩余时间')}: + + {Math.floor(details.compute_minutes_remaining / 60)}h{' '} + {details.compute_minutes_remaining % 60}m
-
-
- {t('创建时间')}: +
+
+ {t('创建时间')}: {timestamp2string(details.created_at)}
-
- {t('最后更新')}: +
+ {t('最后更新')}: {timestamp2string(details.updated_at)}
@@ -505,7 +589,7 @@ const ViewDetailsModal = ({
) : ( - diff --git a/web/src/components/table/model-deployments/modals/ViewLogsModal.jsx b/web/src/components/table/model-deployments/modals/ViewLogsModal.jsx index 18eb5535b..3d0446aea 100644 --- a/web/src/components/table/model-deployments/modals/ViewLogsModal.jsx +++ b/web/src/components/table/model-deployments/modals/ViewLogsModal.jsx @@ -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 ( - + {t('未知状态')} ); } - 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 ( - + {t(config.label)} ); }; - 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) => (
{line}
@@ -362,10 +393,10 @@ const ViewLogsModal = ({ return ( - +
+ {t('容器日志')} - + - {deployment?.container_name || deployment?.id}
@@ -375,13 +406,13 @@ const ViewLogsModal = ({ footer={null} width={1000} height={700} - className="logs-modal" + className='logs-modal' style={{ top: 20 }} > -
+
{/* Controls */} - -
+ +