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:
@@ -1,6 +1,8 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -23,6 +25,20 @@ func getIoAPIKey(c *gin.Context) (string, bool) {
|
|||||||
return apiKey, true
|
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) {
|
func getIoClient(c *gin.Context) (*ionet.Client, bool) {
|
||||||
apiKey, ok := getIoAPIKey(c)
|
apiKey, ok := getIoAPIKey(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -44,15 +60,28 @@ func TestIoNetConnection(c *gin.Context) {
|
|||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
rawBody, err := c.GetRawData()
|
||||||
common.ApiErrorMsg(c, "invalid request payload")
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
return
|
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)
|
apiKey := strings.TrimSpace(req.APIKey)
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
common.ApiErrorMsg(c, "api_key is required")
|
common.OptionMapRWMutex.RLock()
|
||||||
return
|
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)
|
client := ionet.NewEnterpriseClient(apiKey)
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ func GetOptions(c *gin.Context) {
|
|||||||
var options []*model.Option
|
var options []*model.Option
|
||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
for k, v := range common.OptionMap {
|
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
|
continue
|
||||||
}
|
}
|
||||||
options = append(options, &model.Option{
|
options = append(options, &model.Option{
|
||||||
|
|||||||
+2
-16
@@ -269,24 +269,18 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
deploymentsRoute := apiRouter.Group("/deployments")
|
deploymentsRoute := apiRouter.Group("/deployments")
|
||||||
deploymentsRoute.Use(middleware.AdminAuth())
|
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("/", controller.GetAllDeployments)
|
||||||
deploymentsRoute.GET("/search", controller.SearchDeployments)
|
deploymentsRoute.GET("/search", controller.SearchDeployments)
|
||||||
|
|
||||||
// Connection utilities
|
|
||||||
deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
|
deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
|
||||||
|
|
||||||
// Resource and configuration endpoints
|
|
||||||
deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
|
deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
|
||||||
deploymentsRoute.GET("/locations", controller.GetLocations)
|
deploymentsRoute.GET("/locations", controller.GetLocations)
|
||||||
deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
|
deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
|
||||||
deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
|
deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
|
||||||
deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
|
deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
|
||||||
|
|
||||||
// Create new deployment
|
|
||||||
deploymentsRoute.POST("/", controller.CreateDeployment)
|
deploymentsRoute.POST("/", controller.CreateDeployment)
|
||||||
|
|
||||||
// Individual deployment operations
|
|
||||||
deploymentsRoute.GET("/:id", controller.GetDeployment)
|
deploymentsRoute.GET("/:id", controller.GetDeployment)
|
||||||
deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
|
deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
|
||||||
deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
|
deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
|
||||||
@@ -295,14 +289,6 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
|
deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
|
||||||
deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
|
deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
|
||||||
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export default defineConfig({
|
|||||||
"zh",
|
"zh",
|
||||||
"en",
|
"en",
|
||||||
"fr",
|
"fr",
|
||||||
"ru"
|
"ru",
|
||||||
|
"ja",
|
||||||
|
"vi"
|
||||||
],
|
],
|
||||||
extract: {
|
extract: {
|
||||||
input: [
|
input: [
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const DeploymentAccessGuard = ({
|
|||||||
<div className='mt-[60px] px-2'>
|
<div className='mt-[60px] px-2'>
|
||||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||||
<Text type="secondary">{t('加载设置中...')}</Text>
|
<Text type='secondary'>{t('加载设置中...')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@ const DeploymentAccessGuard = ({
|
|||||||
minHeight: 'calc(100vh - 60px)',
|
minHeight: 'calc(100vh - 60px)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -69,7 +69,7 @@ const DeploymentAccessGuard = ({
|
|||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '0 20px'
|
padding: '0 20px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
@@ -78,23 +78,27 @@ const DeploymentAccessGuard = ({
|
|||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
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%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 图标区域 */}
|
{/* 图标区域 */}
|
||||||
<div style={{ marginBottom: '32px' }}>
|
<div style={{ marginBottom: '32px' }}>
|
||||||
<div style={{
|
<div
|
||||||
display: 'inline-flex',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'inline-flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
width: '120px',
|
justifyContent: 'center',
|
||||||
height: '120px',
|
width: '120px',
|
||||||
borderRadius: '50%',
|
height: '120px',
|
||||||
background: 'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
|
borderRadius: '50%',
|
||||||
border: '3px solid rgba(var(--semi-orange-4), 0.3)',
|
background:
|
||||||
marginBottom: '24px'
|
'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
|
||||||
}}>
|
border: '3px solid rgba(var(--semi-orange-4), 0.3)',
|
||||||
<AlertCircle size={56} color="var(--semi-color-warning)" />
|
marginBottom: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertCircle size={56} color='var(--semi-color-warning)' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +110,7 @@ const DeploymentAccessGuard = ({
|
|||||||
color: 'var(--semi-color-text-0)',
|
color: 'var(--semi-color-text-0)',
|
||||||
margin: '0 0 12px 0',
|
margin: '0 0 12px 0',
|
||||||
fontSize: '28px',
|
fontSize: '28px',
|
||||||
fontWeight: '700'
|
fontWeight: '700',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('模型部署服务未启用')}
|
{t('模型部署服务未启用')}
|
||||||
@@ -116,7 +120,7 @@ const DeploymentAccessGuard = ({
|
|||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
color: 'var(--semi-color-text-1)',
|
color: 'var(--semi-color-text-1)',
|
||||||
display: 'block'
|
display: 'block',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('访问模型部署功能需要先启用 io.net 部署服务')}
|
{t('访问模型部署功能需要先启用 io.net 部署服务')}
|
||||||
@@ -131,68 +135,92 @@ const DeploymentAccessGuard = ({
|
|||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
margin: '32px 0',
|
margin: '32px 0',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
marginBottom: '16px'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '32px',
|
gap: '12px',
|
||||||
height: '32px',
|
marginBottom: '16px',
|
||||||
borderRadius: '8px',
|
}}
|
||||||
backgroundColor: 'rgba(var(--semi-blue-4), 0.15)'
|
>
|
||||||
}}>
|
<div
|
||||||
<Server size={20} color="var(--semi-color-primary)" />
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: 'rgba(var(--semi-blue-4), 0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Server size={20} color='var(--semi-color-primary)' />
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Text
|
||||||
strong
|
strong
|
||||||
style={{
|
style={{
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
color: 'var(--semi-color-text-0)'
|
color: 'var(--semi-color-text-0)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('需要配置的项目')}
|
{t('需要配置的项目')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
gap: '12px',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
gap: '12px',
|
||||||
textAlign: 'left',
|
alignItems: 'flex-start',
|
||||||
maxWidth: '320px',
|
textAlign: 'left',
|
||||||
margin: '0 auto'
|
maxWidth: '320px',
|
||||||
}}>
|
margin: '0 auto',
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
}}
|
||||||
<div style={{
|
>
|
||||||
width: '6px',
|
<div
|
||||||
height: '6px',
|
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
|
||||||
borderRadius: '50%',
|
>
|
||||||
backgroundColor: 'var(--semi-color-primary)',
|
<div
|
||||||
flexShrink: 0
|
style={{
|
||||||
}}></div>
|
width: '6px',
|
||||||
<Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
|
height: '6px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'var(--semi-color-primary)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
color: 'var(--semi-color-text-1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('启用 io.net 部署开关')}
|
{t('启用 io.net 部署开关')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div
|
||||||
<div style={{
|
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
|
||||||
width: '6px',
|
>
|
||||||
height: '6px',
|
<div
|
||||||
borderRadius: '50%',
|
style={{
|
||||||
backgroundColor: 'var(--semi-color-primary)',
|
width: '6px',
|
||||||
flexShrink: 0
|
height: '6px',
|
||||||
}}></div>
|
borderRadius: '50%',
|
||||||
<Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
|
backgroundColor: 'var(--semi-color-primary)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
color: 'var(--semi-color-text-1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('配置有效的 io.net API Key')}
|
{t('配置有效的 io.net API Key')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,17 +244,18 @@ const DeploymentAccessGuard = ({
|
|||||||
background: 'var(--semi-color-fill-0)',
|
background: 'var(--semi-color-fill-0)',
|
||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
textDecoration: 'none'
|
textDecoration: 'none',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.target.style.background = 'var(--semi-color-fill-1)';
|
e.currentTarget.style.background = 'var(--semi-color-fill-1)';
|
||||||
e.target.style.transform = 'translateY(-1px)';
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
e.target.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
|
e.currentTarget.style.boxShadow =
|
||||||
|
'0 2px 8px rgba(0, 0, 0, 0.1)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.target.style.background = 'var(--semi-color-fill-0)';
|
e.currentTarget.style.background = 'var(--semi-color-fill-0)';
|
||||||
e.target.style.transform = 'translateY(0)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.target.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
@@ -236,11 +265,11 @@ const DeploymentAccessGuard = ({
|
|||||||
|
|
||||||
{/* 底部提示 */}
|
{/* 底部提示 */}
|
||||||
<Text
|
<Text
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: 'var(--semi-color-text-2)',
|
color: 'var(--semi-color-text-2)',
|
||||||
lineHeight: '1.5'
|
lineHeight: '1.5',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('配置完成后刷新页面即可使用模型部署功能')}
|
{t('配置完成后刷新页面即可使用模型部署功能')}
|
||||||
@@ -256,7 +285,7 @@ const DeploymentAccessGuard = ({
|
|||||||
<div className='mt-[60px] px-2'>
|
<div className='mt-[60px] px-2'>
|
||||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||||
<Text type="secondary">{t('Checking io.net connection...')}</Text>
|
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,12 +294,10 @@ const DeploymentAccessGuard = ({
|
|||||||
|
|
||||||
if (connectionOk === false) {
|
if (connectionOk === false) {
|
||||||
const isExpired = connectionError?.type === 'expired';
|
const isExpired = connectionError?.type === 'expired';
|
||||||
const title = isExpired
|
const title = isExpired ? t('接口密钥已过期') : t('无法连接 io.net');
|
||||||
? t('API key expired')
|
|
||||||
: t('io.net connection unavailable');
|
|
||||||
const description = isExpired
|
const description = isExpired
|
||||||
? t('The current API key is expired. Please update it in settings.')
|
? t('当前 API 密钥已过期,请在设置中更新。')
|
||||||
: t('Unable to connect to io.net with the current configuration.');
|
: t('当前配置无法连接到 io.net。');
|
||||||
const detail = connectionError?.message || '';
|
const detail = connectionError?.message || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -297,7 +324,8 @@ const DeploymentAccessGuard = ({
|
|||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
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%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: '32px' }}>
|
<div style={{ marginBottom: '32px' }}>
|
||||||
@@ -309,12 +337,13 @@ const DeploymentAccessGuard = ({
|
|||||||
width: '120px',
|
width: '120px',
|
||||||
height: '120px',
|
height: '120px',
|
||||||
borderRadius: '50%',
|
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)',
|
border: '3px solid rgba(var(--semi-red-4), 0.3)',
|
||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WifiOff size={56} color="var(--semi-color-danger)" />
|
<WifiOff size={56} color='var(--semi-color-danger)' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -342,7 +371,7 @@ const DeploymentAccessGuard = ({
|
|||||||
</Text>
|
</Text>
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<Text
|
<Text
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
@@ -355,13 +384,19 @@ const DeploymentAccessGuard = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
<div
|
||||||
<Button type="primary" icon={<Settings size={18} />} onClick={handleGoToSettings}>
|
style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}
|
||||||
{t('Go to settings')}
|
>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<Settings size={18} />}
|
||||||
|
onClick={handleGoToSettings}
|
||||||
|
>
|
||||||
|
{t('前往设置')}
|
||||||
</Button>
|
</Button>
|
||||||
{onRetry ? (
|
{onRetry ? (
|
||||||
<Button type="tertiary" onClick={onRetry}>
|
<Button type='tertiary' onClick={onRetry}>
|
||||||
{t('Retry connection')}
|
{t('重试连接')}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ import CodeViewer from '../../../playground/CodeViewer';
|
|||||||
import { StatusContext } from '../../../../context/Status';
|
import { StatusContext } from '../../../../context/Status';
|
||||||
import { UserContext } from '../../../../context/User';
|
import { UserContext } from '../../../../context/User';
|
||||||
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
|
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
|
||||||
import { useSidebar } from '../../../../hooks/common/useSidebar';
|
import {
|
||||||
|
mergeAdminConfig,
|
||||||
|
useSidebar,
|
||||||
|
} from '../../../../hooks/common/useSidebar';
|
||||||
|
|
||||||
const NotificationSettings = ({
|
const NotificationSettings = ({
|
||||||
t,
|
t,
|
||||||
@@ -82,6 +85,7 @@ const NotificationSettings = ({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
channel: true,
|
channel: true,
|
||||||
models: true,
|
models: true,
|
||||||
|
deployment: true,
|
||||||
redemption: true,
|
redemption: true,
|
||||||
user: true,
|
user: true,
|
||||||
setting: true,
|
setting: true,
|
||||||
@@ -164,6 +168,7 @@ const NotificationSettings = ({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
channel: true,
|
channel: true,
|
||||||
models: true,
|
models: true,
|
||||||
|
deployment: true,
|
||||||
redemption: true,
|
redemption: true,
|
||||||
user: true,
|
user: true,
|
||||||
setting: true,
|
setting: true,
|
||||||
@@ -178,14 +183,27 @@ const NotificationSettings = ({
|
|||||||
try {
|
try {
|
||||||
// 获取管理员全局配置
|
// 获取管理员全局配置
|
||||||
if (statusState?.status?.SidebarModulesAdmin) {
|
if (statusState?.status?.SidebarModulesAdmin) {
|
||||||
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
|
try {
|
||||||
setAdminConfig(adminConf);
|
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');
|
const userRes = await API.get('/api/user/self');
|
||||||
if (userRes.data.success && userRes.data.data.sidebar_modules) {
|
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);
|
setSidebarModulesUser(userConf);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -273,6 +291,11 @@ const NotificationSettings = ({
|
|||||||
modules: [
|
modules: [
|
||||||
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
||||||
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
||||||
|
{
|
||||||
|
key: 'deployment',
|
||||||
|
title: t('模型部署'),
|
||||||
|
description: t('模型部署管理'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'redemption',
|
key: 'redemption',
|
||||||
title: t('兑换码管理'),
|
title: t('兑换码管理'),
|
||||||
@@ -812,7 +835,9 @@ const NotificationSettings = ({
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={sidebarModulesUser[section.key]?.enabled}
|
checked={
|
||||||
|
sidebarModulesUser[section.key]?.enabled !== false
|
||||||
|
}
|
||||||
onChange={handleSectionChange(section.key)}
|
onChange={handleSectionChange(section.key)}
|
||||||
size='default'
|
size='default'
|
||||||
/>
|
/>
|
||||||
@@ -835,7 +860,8 @@ const NotificationSettings = ({
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
||||||
sidebarModulesUser[section.key]?.enabled
|
sidebarModulesUser[section.key]?.enabled !==
|
||||||
|
false
|
||||||
? ''
|
? ''
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
}`}
|
}`}
|
||||||
@@ -866,7 +892,7 @@ const NotificationSettings = ({
|
|||||||
checked={
|
checked={
|
||||||
sidebarModulesUser[section.key]?.[
|
sidebarModulesUser[section.key]?.[
|
||||||
module.key
|
module.key
|
||||||
]
|
] !== false
|
||||||
}
|
}
|
||||||
onChange={handleModuleChange(
|
onChange={handleModuleChange(
|
||||||
section.key,
|
section.key,
|
||||||
@@ -874,8 +900,8 @@ const NotificationSettings = ({
|
|||||||
)}
|
)}
|
||||||
size='default'
|
size='default'
|
||||||
disabled={
|
disabled={
|
||||||
!sidebarModulesUser[section.key]
|
sidebarModulesUser[section.key]
|
||||||
?.enabled
|
?.enabled === false
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,30 +30,24 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Tag,
|
Tag,
|
||||||
Avatar,
|
|
||||||
Empty,
|
Empty,
|
||||||
Divider,
|
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Progress,
|
Progress,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Radio,
|
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconClose,
|
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconServer,
|
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
authHeader,
|
authHeader,
|
||||||
getUserIdFromLocalStorage,
|
getUserIdFromLocalStorage,
|
||||||
showError,
|
showError,
|
||||||
showInfo,
|
|
||||||
showSuccess,
|
showSuccess,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
|
|
||||||
@@ -85,9 +79,7 @@ const resolveOllamaBaseUrl = (info) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alt =
|
const alt =
|
||||||
typeof info.ollama_base_url === 'string'
|
typeof info.ollama_base_url === 'string' ? info.ollama_base_url.trim() : '';
|
||||||
? info.ollama_base_url.trim()
|
|
||||||
: '';
|
|
||||||
if (alt) {
|
if (alt) {
|
||||||
return alt;
|
return alt;
|
||||||
}
|
}
|
||||||
@@ -125,7 +117,8 @@ const normalizeModels = (items) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof item === 'object') {
|
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) {
|
if (!candidateId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -147,7 +140,10 @@ const normalizeModels = (items) => {
|
|||||||
if (!normalized.digest && typeof metadata.digest === 'string') {
|
if (!normalized.digest && typeof metadata.digest === 'string') {
|
||||||
normalized.digest = metadata.digest;
|
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;
|
normalized.modified_at = metadata.modified_at;
|
||||||
}
|
}
|
||||||
if (metadata.details && !normalized.details) {
|
if (metadata.details && !normalized.details) {
|
||||||
@@ -440,7 +436,6 @@ const OllamaModelModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
await processStream();
|
await processStream();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== 'AbortError') {
|
if (error?.name !== 'AbortError') {
|
||||||
showError(t('模型拉取失败: {{error}}', { error: error.message }));
|
showError(t('模型拉取失败: {{error}}', { error: error.message }));
|
||||||
@@ -481,8 +476,8 @@ const OllamaModelModal = ({
|
|||||||
if (!searchValue) {
|
if (!searchValue) {
|
||||||
setFilteredModels(models);
|
setFilteredModels(models);
|
||||||
} else {
|
} else {
|
||||||
const filtered = models.filter(model =>
|
const filtered = models.filter((model) =>
|
||||||
model.id.toLowerCase().includes(searchValue.toLowerCase())
|
model.id.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
);
|
);
|
||||||
setFilteredModels(filtered);
|
setFilteredModels(filtered);
|
||||||
}
|
}
|
||||||
@@ -527,59 +522,37 @@ const OllamaModelModal = ({
|
|||||||
const formatModelSize = (size) => {
|
const formatModelSize = (size) => {
|
||||||
if (!size) return '-';
|
if (!size) return '-';
|
||||||
const gb = size / (1024 * 1024 * 1024);
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={t('Ollama 模型管理')}
|
||||||
<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>
|
|
||||||
}
|
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
width={800}
|
width={720}
|
||||||
style={{ maxWidth: '95vw' }}
|
style={{ maxWidth: '95vw' }}
|
||||||
footer={
|
footer={
|
||||||
<div className='flex justify-end'>
|
<Button theme='solid' type='primary' onClick={onCancel}>
|
||||||
<Button
|
{t('关闭')}
|
||||||
theme='light'
|
</Button>
|
||||||
type='primary'
|
|
||||||
onClick={onCancel}
|
|
||||||
icon={<IconClose />}
|
|
||||||
>
|
|
||||||
{t('关闭')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<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'>
|
<Card>
|
||||||
<div className='flex items-center mb-4'>
|
<Title heading={6} className='m-0 mb-3'>
|
||||||
<Avatar size='small' color='green' className='mr-2'>
|
{t('拉取新模型')}
|
||||||
<IconPlus size={16} />
|
</Title>
|
||||||
</Avatar>
|
|
||||||
<Title heading={5} className='m-0'>
|
|
||||||
{t('拉取新模型')}
|
|
||||||
</Title>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Row gutter={12} align='middle'>
|
<Row gutter={12} align='middle'>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
@@ -608,74 +581,79 @@ const OllamaModelModal = ({
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* 进度条显示 */}
|
{/* 进度条显示 */}
|
||||||
{pullProgress && (() => {
|
{pullProgress &&
|
||||||
const completedBytes = Number(pullProgress.completed) || 0;
|
(() => {
|
||||||
const totalBytes = Number(pullProgress.total) || 0;
|
const completedBytes = Number(pullProgress.completed) || 0;
|
||||||
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
|
const totalBytes = Number(pullProgress.total) || 0;
|
||||||
const safePercent = hasTotal
|
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
|
||||||
? Math.min(
|
const safePercent = hasTotal
|
||||||
100,
|
? Math.min(
|
||||||
Math.max(0, Math.round((completedBytes / totalBytes) * 100)),
|
100,
|
||||||
)
|
Math.max(
|
||||||
: null;
|
0,
|
||||||
const percentText = hasTotal && safePercent !== null
|
Math.round((completedBytes / totalBytes) * 100),
|
||||||
? `${safePercent.toFixed(0)}%`
|
),
|
||||||
: pullProgress.status || t('处理中');
|
)
|
||||||
|
: null;
|
||||||
|
const percentText =
|
||||||
|
hasTotal && safePercent !== null
|
||||||
|
? `${safePercent.toFixed(0)}%`
|
||||||
|
: pullProgress.status || t('处理中');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-3 p-3 bg-gray-50 rounded-lg'>
|
<div style={{ marginTop: 12 }}>
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Text strong>{t('拉取进度')}</Text>
|
<Text strong>{t('拉取进度')}</Text>
|
||||||
<Text type='tertiary' size='small'>{percentText}</Text>
|
<Text type='tertiary' size='small'>
|
||||||
</div>
|
{percentText}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasTotal && safePercent !== null ? (
|
{hasTotal && safePercent !== null ? (
|
||||||
<div>
|
<div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={safePercent}
|
percent={safePercent}
|
||||||
showInfo={false}
|
showInfo={false}
|
||||||
stroke='#1890ff'
|
stroke='#1890ff'
|
||||||
size='small'
|
size='small'
|
||||||
/>
|
/>
|
||||||
<div className='flex justify-between mt-1'>
|
<div className='flex justify-between mt-1'>
|
||||||
<Text type='tertiary' size='small'>
|
<Text type='tertiary' size='small'>
|
||||||
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)}{' '}
|
||||||
</Text>
|
GB
|
||||||
<Text type='tertiary' size='small'>
|
</Text>
|
||||||
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
<Text type='tertiary' size='small'>
|
||||||
</Text>
|
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
|
||||||
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
|
<Spin size='small' />
|
||||||
<Spin size='small' />
|
<span>{t('准备中...')}</span>
|
||||||
<span>{t('准备中...')}</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})()}
|
||||||
})()}
|
|
||||||
|
|
||||||
<Text type='tertiary' size='small' className='mt-2 block'>
|
<Text type='tertiary' size='small' className='mt-2 block'>
|
||||||
{t('支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间')}
|
{t(
|
||||||
|
'支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 已有模型列表 */}
|
{/* 已有模型列表 */}
|
||||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
<Card>
|
||||||
<div className='flex items-center justify-between mb-4'>
|
<div className='flex items-center justify-between mb-3'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center gap-2'>
|
||||||
<Avatar size='small' color='purple' className='mr-2'>
|
<Title heading={6} className='m-0'>
|
||||||
<IconServer size={16} />
|
|
||||||
</Avatar>
|
|
||||||
<Title heading={5} className='m-0'>
|
|
||||||
{t('已有模型')}
|
{t('已有模型')}
|
||||||
{models.length > 0 && (
|
|
||||||
<Tag color='blue' className='ml-2'>
|
|
||||||
{models.length}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Title>
|
</Title>
|
||||||
|
{models.length > 0 ? (
|
||||||
|
<Tag color='blue'>{models.length}</Tag>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Input
|
<Input
|
||||||
@@ -688,7 +666,7 @@ const OllamaModelModal = ({
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
theme='borderless'
|
theme='light'
|
||||||
onClick={handleSelectAll}
|
onClick={handleSelectAll}
|
||||||
disabled={models.length === 0}
|
disabled={models.length === 0}
|
||||||
>
|
>
|
||||||
@@ -696,7 +674,7 @@ const OllamaModelModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
theme='borderless'
|
theme='light'
|
||||||
onClick={handleClearSelection}
|
onClick={handleClearSelection}
|
||||||
disabled={selectedModelIds.length === 0}
|
disabled={selectedModelIds.length === 0}
|
||||||
>
|
>
|
||||||
@@ -728,7 +706,6 @@ const OllamaModelModal = ({
|
|||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{filteredModels.length === 0 ? (
|
{filteredModels.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
image={<IconServer size={60} />}
|
|
||||||
title={searchValue ? t('未找到匹配的模型') : t('暂无模型')}
|
title={searchValue ? t('未找到匹配的模型') : t('暂无模型')}
|
||||||
description={
|
description={
|
||||||
searchValue
|
searchValue
|
||||||
@@ -740,25 +717,17 @@ const OllamaModelModal = ({
|
|||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
dataSource={filteredModels}
|
dataSource={filteredModels}
|
||||||
split={false}
|
split
|
||||||
renderItem={(model, index) => (
|
renderItem={(model) => (
|
||||||
<List.Item
|
<List.Item key={model.id}>
|
||||||
key={model.id}
|
|
||||||
className='hover:bg-gray-50 rounded-lg p-3 transition-colors'
|
|
||||||
>
|
|
||||||
<div className='flex items-center justify-between w-full'>
|
<div className='flex items-center justify-between w-full'>
|
||||||
<div className='flex items-center flex-1 min-w-0 gap-3'>
|
<div className='flex items-center flex-1 min-w-0 gap-3'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedModelIds.includes(model.id)}
|
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'>
|
<div className='flex-1 min-w-0'>
|
||||||
<Text strong className='block truncate'>
|
<Text strong className='block truncate'>
|
||||||
{model.id}
|
{model.id}
|
||||||
@@ -775,10 +744,13 @@ const OllamaModelModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center space-x-2 ml-4'>
|
<div className='flex items-center space-x-2 ml-4'>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('确认删除模型')}
|
title={t('确认删除模型')}
|
||||||
content={t('删除后无法恢复,确定要删除模型 "{{name}}" 吗?', { name: model.id })}
|
content={t(
|
||||||
|
'删除后无法恢复,确定要删除模型 "{{name}}" 吗?',
|
||||||
|
{ name: model.id },
|
||||||
|
)}
|
||||||
onConfirm={() => deleteModel(model.id)}
|
onConfirm={() => deleteModel(model.id)}
|
||||||
okText={t('确认')}
|
okText={t('确认')}
|
||||||
cancelText={t('取消')}
|
cancelText={t('取消')}
|
||||||
@@ -798,7 +770,7 @@ const OllamaModelModal = ({
|
|||||||
)}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ const DeploymentsActions = ({
|
|||||||
setEditingDeployment,
|
setEditingDeployment,
|
||||||
setShowEdit,
|
setShowEdit,
|
||||||
batchDeleteDeployments,
|
batchDeleteDeployments,
|
||||||
|
batchOperationsEnabled = true,
|
||||||
compactMode,
|
compactMode,
|
||||||
setCompactMode,
|
setCompactMode,
|
||||||
showCreateModal,
|
showCreateModal,
|
||||||
setShowCreateModal,
|
setShowCreateModal,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const hasSelected = selectedKeys.length > 0;
|
const hasSelected = batchOperationsEnabled && selectedKeys.length > 0;
|
||||||
|
|
||||||
const handleAddDeployment = () => {
|
const handleAddDeployment = () => {
|
||||||
if (setShowCreateModal) {
|
if (setShowCreateModal) {
|
||||||
@@ -53,7 +54,6 @@ const DeploymentsActions = ({
|
|||||||
setSelectedKeys([]);
|
setSelectedKeys([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
|
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Button, Dropdown, Tag, Typography } from '@douyinfe/semi-ui';
|
||||||
Button,
|
import { timestamp2string, showSuccess, showError } from '../../../helpers';
|
||||||
Dropdown,
|
|
||||||
Tag,
|
|
||||||
Typography,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
timestamp2string,
|
|
||||||
showSuccess,
|
|
||||||
showError,
|
|
||||||
} from '../../../helpers';
|
|
||||||
import { IconMore } from '@douyinfe/semi-icons';
|
import { IconMore } from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
FaPlay,
|
FaPlay,
|
||||||
@@ -50,7 +41,6 @@ import {
|
|||||||
FaHourglassHalf,
|
FaHourglassHalf,
|
||||||
FaGlobe,
|
FaGlobe,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import {t} from "i18next";
|
|
||||||
|
|
||||||
const normalizeStatus = (status) =>
|
const normalizeStatus = (status) =>
|
||||||
typeof status === 'string' ? status.trim().toLowerCase() : '';
|
typeof status === 'string' ? status.trim().toLowerCase() : '';
|
||||||
@@ -58,59 +48,59 @@ const normalizeStatus = (status) =>
|
|||||||
const STATUS_TAG_CONFIG = {
|
const STATUS_TAG_CONFIG = {
|
||||||
running: {
|
running: {
|
||||||
color: 'green',
|
color: 'green',
|
||||||
label: t('运行中'),
|
labelKey: '运行中',
|
||||||
icon: <FaPlay size={12} className='text-green-600' />,
|
icon: <FaPlay size={12} className='text-green-600' />,
|
||||||
},
|
},
|
||||||
deploying: {
|
deploying: {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
label: t('部署中'),
|
labelKey: '部署中',
|
||||||
icon: <FaSpinner size={12} className='text-blue-600' />,
|
icon: <FaSpinner size={12} className='text-blue-600' />,
|
||||||
},
|
},
|
||||||
pending: {
|
pending: {
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
label: t('待部署'),
|
labelKey: '待部署',
|
||||||
icon: <FaClock size={12} className='text-orange-600' />,
|
icon: <FaClock size={12} className='text-orange-600' />,
|
||||||
},
|
},
|
||||||
stopped: {
|
stopped: {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
label: t('已停止'),
|
labelKey: '已停止',
|
||||||
icon: <FaStop size={12} className='text-gray-500' />,
|
icon: <FaStop size={12} className='text-gray-500' />,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: 'red',
|
color: 'red',
|
||||||
label: t('错误'),
|
labelKey: '错误',
|
||||||
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
color: 'red',
|
color: 'red',
|
||||||
label: t('失败'),
|
labelKey: '失败',
|
||||||
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
icon: <FaExclamationCircle size={12} className='text-red-500' />,
|
||||||
},
|
},
|
||||||
destroyed: {
|
destroyed: {
|
||||||
color: 'red',
|
color: 'red',
|
||||||
label: t('已销毁'),
|
labelKey: '已销毁',
|
||||||
icon: <FaBan size={12} className='text-red-500' />,
|
icon: <FaBan size={12} className='text-red-500' />,
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
color: 'green',
|
color: 'green',
|
||||||
label: t('已完成'),
|
labelKey: '已完成',
|
||||||
icon: <FaCheckCircle size={12} className='text-green-600' />,
|
icon: <FaCheckCircle size={12} className='text-green-600' />,
|
||||||
},
|
},
|
||||||
'deployment requested': {
|
'deployment requested': {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
label: t('部署请求中'),
|
labelKey: '部署请求中',
|
||||||
icon: <FaSpinner size={12} className='text-blue-600' />,
|
icon: <FaSpinner size={12} className='text-blue-600' />,
|
||||||
},
|
},
|
||||||
'termination requested': {
|
'termination requested': {
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
label: t('终止请求中'),
|
labelKey: '终止请求中',
|
||||||
icon: <FaClock size={12} className='text-orange-600' />,
|
icon: <FaClock size={12} className='text-orange-600' />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_STATUS_CONFIG = {
|
const DEFAULT_STATUS_CONFIG = {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
label: null,
|
labelKey: null,
|
||||||
icon: <FaInfoCircle size={12} className='text-gray-500' />,
|
icon: <FaInfoCircle size={12} className='text-gray-500' />,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,7 +180,9 @@ const renderStatus = (status, t) => {
|
|||||||
const normalizedStatus = normalizeStatus(status);
|
const normalizedStatus = normalizeStatus(status);
|
||||||
const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
|
const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
|
||||||
const statusText = typeof status === 'string' ? status : '';
|
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 (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
@@ -206,20 +198,24 @@ const renderStatus = (status, t) => {
|
|||||||
|
|
||||||
// Container Name Cell Component - to properly handle React hooks
|
// Container Name Cell Component - to properly handle React hooks
|
||||||
const ContainerNameCell = ({ text, record, t }) => {
|
const ContainerNameCell = ({ text, record, t }) => {
|
||||||
const handleCopyId = () => {
|
const handleCopyId = async () => {
|
||||||
navigator.clipboard.writeText(record.id);
|
try {
|
||||||
showSuccess(t('ID已复制到剪贴板'));
|
await navigator.clipboard.writeText(record.id);
|
||||||
|
showSuccess(t('已复制 ID 到剪贴板'));
|
||||||
|
} catch (err) {
|
||||||
|
showError(t('复制失败'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
<Typography.Text strong className="text-base">
|
<Typography.Text strong className='text-base'>
|
||||||
{text}
|
{text}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
type="secondary"
|
type='secondary'
|
||||||
size="small"
|
size='small'
|
||||||
className="text-xs cursor-pointer hover:text-blue-600 transition-colors select-all"
|
className='text-xs cursor-pointer hover:text-blue-600 transition-colors select-all'
|
||||||
onClick={handleCopyId}
|
onClick={handleCopyId}
|
||||||
title={t('点击复制ID')}
|
title={t('点击复制ID')}
|
||||||
>
|
>
|
||||||
@@ -236,22 +232,22 @@ const renderResourceConfig = (resource, t) => {
|
|||||||
const { cpu, memory, gpu } = resource;
|
const { cpu, memory, gpu } = resource;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
{cpu && (
|
{cpu && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className='flex items-center gap-1 text-xs'>
|
||||||
<FaMicrochip className="text-blue-500" />
|
<FaMicrochip className='text-blue-500' />
|
||||||
<span>CPU: {cpu}</span>
|
<span>CPU: {cpu}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{memory && (
|
{memory && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className='flex items-center gap-1 text-xs'>
|
||||||
<FaMemory className="text-green-500" />
|
<FaMemory className='text-green-500' />
|
||||||
<span>内存: {memory}</span>
|
<span>内存: {memory}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{gpu && (
|
{gpu && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className='flex items-center gap-1 text-xs'>
|
||||||
<FaServer className="text-purple-500" />
|
<FaServer className='text-purple-500' />
|
||||||
<span>GPU: {gpu}</span>
|
<span>GPU: {gpu}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -266,7 +262,7 @@ const renderInstanceCount = (count, record, t) => {
|
|||||||
const countColor = statusConfig?.color ?? 'grey';
|
const countColor = statusConfig?.color ?? 'grey';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag color={countColor} size="small" shape='circle'>
|
<Tag color={countColor} size='small' shape='circle'>
|
||||||
{count || 0} {t('个实例')}
|
{count || 0} {t('个实例')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -299,11 +295,7 @@ export const getDeploymentsColumns = ({
|
|||||||
width: 300,
|
width: 300,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<ContainerNameCell
|
<ContainerNameCell text={text} record={record} t={t} />
|
||||||
text={text}
|
|
||||||
record={record}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -312,9 +304,7 @@ export const getDeploymentsColumns = ({
|
|||||||
key: COLUMN_KEYS.status,
|
key: COLUMN_KEYS.status,
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (status) => (
|
render: (status) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>{renderStatus(status, t)}</div>
|
||||||
{renderStatus(status, t)}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -325,18 +315,22 @@ export const getDeploymentsColumns = ({
|
|||||||
render: (provider) =>
|
render: (provider) =>
|
||||||
provider ? (
|
provider ? (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
borderColor: 'rgba(59, 130, 246, 0.4)',
|
borderColor: 'rgba(59, 130, 246, 0.4)',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
color: '#2563eb',
|
color: '#2563eb',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaGlobe className="text-[11px]" />
|
<FaGlobe className='text-[11px]' />
|
||||||
<span>{provider}</span>
|
<span>{provider}</span>
|
||||||
</div>
|
</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('暂无')}
|
{t('暂无')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
),
|
),
|
||||||
@@ -345,7 +339,7 @@ export const getDeploymentsColumns = ({
|
|||||||
title: t('剩余时间'),
|
title: t('剩余时间'),
|
||||||
dataIndex: 'time_remaining',
|
dataIndex: 'time_remaining',
|
||||||
key: COLUMN_KEYS.time_remaining,
|
key: COLUMN_KEYS.time_remaining,
|
||||||
width: 140,
|
width: 200,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const normalizedStatus = normalizeStatus(record?.status);
|
const normalizedStatus = normalizeStatus(record?.status);
|
||||||
const percentUsedRaw = parsePercentValue(record?.completed_percent);
|
const percentUsedRaw = parsePercentValue(record?.completed_percent);
|
||||||
@@ -380,43 +374,43 @@ export const getDeploymentsColumns = ({
|
|||||||
percentRemaining !== null;
|
percentRemaining !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 leading-tight text-xs">
|
<div className='flex flex-col gap-1 leading-tight text-xs'>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className='flex items-center gap-1.5'>
|
||||||
<FaHourglassHalf
|
<FaHourglassHalf
|
||||||
className="text-sm"
|
className='text-sm'
|
||||||
style={{ color: theme.iconColor }}
|
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}
|
{timeDisplay}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{showProgress && percentRemaining !== null ? (
|
{showProgress && percentRemaining !== null ? (
|
||||||
<Tag size="small" color={theme.tagColor}>
|
<Tag size='small' color={theme.tagColor}>
|
||||||
{percentRemaining}%
|
{percentRemaining}%
|
||||||
</Tag>
|
</Tag>
|
||||||
) : statusOverride ? (
|
) : statusOverride ? (
|
||||||
<Tag size="small" color="grey">
|
<Tag size='small' color='grey'>
|
||||||
{statusOverride}
|
{statusOverride}
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{showExtraInfo && (
|
{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 && (
|
{humanReadable && (
|
||||||
<span className="flex items-center gap-1">
|
<span className='flex items-center gap-1'>
|
||||||
<FaClock className="text-[11px]" />
|
<FaClock className='text-[11px]' />
|
||||||
{t('约')} {humanReadable}
|
{t('约')} {humanReadable}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{percentUsed !== null && (
|
{percentUsed !== null && (
|
||||||
<span className="flex items-center gap-1">
|
<span className='flex items-center gap-1'>
|
||||||
<FaCheckCircle className="text-[11px]" />
|
<FaCheckCircle className='text-[11px]' />
|
||||||
{t('已用')} {percentUsed}%
|
{t('已用')} {percentUsed}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showProgress && showRemainingMeta && (
|
{showProgress && showRemainingMeta && (
|
||||||
<div className="text-[10px]" style={{ color: theme.textColor }}>
|
<div className='text-[10px]' style={{ color: theme.textColor }}>
|
||||||
{t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
|
{t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -431,14 +425,16 @@ export const getDeploymentsColumns = ({
|
|||||||
width: 220,
|
width: 220,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<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" />
|
<FaServer className='text-green-600 text-xs' />
|
||||||
<span className="text-xs font-medium text-green-700">
|
<span className='text-xs font-medium text-green-700'>
|
||||||
{record.hardware_name}
|
{record.hardware_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -448,7 +444,7 @@ export const getDeploymentsColumns = ({
|
|||||||
key: COLUMN_KEYS.created_at,
|
key: COLUMN_KEYS.created_at,
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (text) => (
|
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) => {
|
render: (_, record) => {
|
||||||
const { status, id } = record;
|
const { status, id } = record;
|
||||||
const normalizedStatus = normalizeStatus(status);
|
const normalizedStatus = normalizeStatus(status);
|
||||||
const isEnded = normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
|
const isEnded =
|
||||||
|
normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
// Use enhanced confirmation dialog
|
// Use enhanced confirmation dialog
|
||||||
@@ -471,7 +468,7 @@ export const getDeploymentsColumns = ({
|
|||||||
switch (normalizedStatus) {
|
switch (normalizedStatus) {
|
||||||
case 'running':
|
case 'running':
|
||||||
return {
|
return {
|
||||||
icon: <FaInfoCircle className="text-xs" />,
|
icon: <FaInfoCircle className='text-xs' />,
|
||||||
text: t('查看详情'),
|
text: t('查看详情'),
|
||||||
onClick: () => onViewDetails?.(record),
|
onClick: () => onViewDetails?.(record),
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
@@ -480,7 +477,7 @@ export const getDeploymentsColumns = ({
|
|||||||
case 'failed':
|
case 'failed':
|
||||||
case 'error':
|
case 'error':
|
||||||
return {
|
return {
|
||||||
icon: <FaPlay className="text-xs" />,
|
icon: <FaPlay className='text-xs' />,
|
||||||
text: t('重试'),
|
text: t('重试'),
|
||||||
onClick: () => startDeployment(id),
|
onClick: () => startDeployment(id),
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
@@ -488,7 +485,7 @@ export const getDeploymentsColumns = ({
|
|||||||
};
|
};
|
||||||
case 'stopped':
|
case 'stopped':
|
||||||
return {
|
return {
|
||||||
icon: <FaPlay className="text-xs" />,
|
icon: <FaPlay className='text-xs' />,
|
||||||
text: t('启动'),
|
text: t('启动'),
|
||||||
onClick: () => startDeployment(id),
|
onClick: () => startDeployment(id),
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
@@ -497,7 +494,7 @@ export const getDeploymentsColumns = ({
|
|||||||
case 'deployment requested':
|
case 'deployment requested':
|
||||||
case 'deploying':
|
case 'deploying':
|
||||||
return {
|
return {
|
||||||
icon: <FaClock className="text-xs" />,
|
icon: <FaClock className='text-xs' />,
|
||||||
text: t('部署中'),
|
text: t('部署中'),
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
@@ -506,7 +503,7 @@ export const getDeploymentsColumns = ({
|
|||||||
};
|
};
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return {
|
return {
|
||||||
icon: <FaClock className="text-xs" />,
|
icon: <FaClock className='text-xs' />,
|
||||||
text: t('待部署'),
|
text: t('待部署'),
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
@@ -515,7 +512,7 @@ export const getDeploymentsColumns = ({
|
|||||||
};
|
};
|
||||||
case 'termination requested':
|
case 'termination requested':
|
||||||
return {
|
return {
|
||||||
icon: <FaClock className="text-xs" />,
|
icon: <FaClock className='text-xs' />,
|
||||||
text: t('终止中'),
|
text: t('终止中'),
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
@@ -526,7 +523,7 @@ export const getDeploymentsColumns = ({
|
|||||||
case 'destroyed':
|
case 'destroyed':
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
icon: <FaInfoCircle className="text-xs" />,
|
icon: <FaInfoCircle className='text-xs' />,
|
||||||
text: t('已结束'),
|
text: t('已结束'),
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
type: 'tertiary',
|
type: 'tertiary',
|
||||||
@@ -542,13 +539,13 @@ export const getDeploymentsColumns = ({
|
|||||||
|
|
||||||
if (isEnded) {
|
if (isEnded) {
|
||||||
return (
|
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
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
onClick={() => onViewDetails?.(record)}
|
onClick={() => onViewDetails?.(record)}
|
||||||
icon={<FaInfoCircle className="text-xs" />}
|
icon={<FaInfoCircle className='text-xs' />}
|
||||||
>
|
>
|
||||||
{t('查看详情')}
|
{t('查看详情')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -558,14 +555,22 @@ export const getDeploymentsColumns = ({
|
|||||||
|
|
||||||
// All actions dropdown with enhanced operations
|
// All actions dropdown with enhanced operations
|
||||||
const dropdownItems = [
|
const dropdownItems = [
|
||||||
<Dropdown.Item key="details" onClick={() => onViewDetails?.(record)} icon={<FaInfoCircle />}>
|
<Dropdown.Item
|
||||||
|
key='details'
|
||||||
|
onClick={() => onViewDetails?.(record)}
|
||||||
|
icon={<FaInfoCircle />}
|
||||||
|
>
|
||||||
{t('查看详情')}
|
{t('查看详情')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isEnded) {
|
if (!isEnded) {
|
||||||
dropdownItems.push(
|
dropdownItems.push(
|
||||||
<Dropdown.Item key="logs" onClick={() => onViewLogs?.(record)} icon={<FaTerminal />}>
|
<Dropdown.Item
|
||||||
|
key='logs'
|
||||||
|
onClick={() => onViewLogs?.(record)}
|
||||||
|
icon={<FaTerminal />}
|
||||||
|
>
|
||||||
{t('查看日志')}
|
{t('查看日志')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
@@ -575,7 +580,11 @@ export const getDeploymentsColumns = ({
|
|||||||
if (normalizedStatus === 'running') {
|
if (normalizedStatus === 'running') {
|
||||||
if (onSyncToChannel) {
|
if (onSyncToChannel) {
|
||||||
managementItems.push(
|
managementItems.push(
|
||||||
<Dropdown.Item key="sync-channel" onClick={() => onSyncToChannel(record)} icon={<FaLink />}>
|
<Dropdown.Item
|
||||||
|
key='sync-channel'
|
||||||
|
onClick={() => onSyncToChannel(record)}
|
||||||
|
icon={<FaLink />}
|
||||||
|
>
|
||||||
{t('同步到渠道')}
|
{t('同步到渠道')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
@@ -583,28 +592,44 @@ export const getDeploymentsColumns = ({
|
|||||||
}
|
}
|
||||||
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
|
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
|
||||||
managementItems.push(
|
managementItems.push(
|
||||||
<Dropdown.Item key="retry" onClick={() => startDeployment(id)} icon={<FaPlay />}>
|
<Dropdown.Item
|
||||||
|
key='retry'
|
||||||
|
onClick={() => startDeployment(id)}
|
||||||
|
icon={<FaPlay />}
|
||||||
|
>
|
||||||
{t('重试')}
|
{t('重试')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (normalizedStatus === 'stopped') {
|
if (normalizedStatus === 'stopped') {
|
||||||
managementItems.push(
|
managementItems.push(
|
||||||
<Dropdown.Item key="start" onClick={() => startDeployment(id)} icon={<FaPlay />}>
|
<Dropdown.Item
|
||||||
|
key='start'
|
||||||
|
onClick={() => startDeployment(id)}
|
||||||
|
icon={<FaPlay />}
|
||||||
|
>
|
||||||
{t('启动')}
|
{t('启动')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (managementItems.length > 0) {
|
if (managementItems.length > 0) {
|
||||||
dropdownItems.push(<Dropdown.Divider key="management-divider" />);
|
dropdownItems.push(<Dropdown.Divider key='management-divider' />);
|
||||||
dropdownItems.push(...managementItems);
|
dropdownItems.push(...managementItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
const configItems = [];
|
const configItems = [];
|
||||||
if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) {
|
if (
|
||||||
|
!isEnded &&
|
||||||
|
(normalizedStatus === 'running' ||
|
||||||
|
normalizedStatus === 'deployment requested')
|
||||||
|
) {
|
||||||
configItems.push(
|
configItems.push(
|
||||||
<Dropdown.Item key="extend" onClick={() => onExtendDuration?.(record)} icon={<FaPlus />}>
|
<Dropdown.Item
|
||||||
|
key='extend'
|
||||||
|
onClick={() => onExtendDuration?.(record)}
|
||||||
|
icon={<FaPlus />}
|
||||||
|
>
|
||||||
{t('延长时长')}
|
{t('延长时长')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
@@ -618,13 +643,18 @@ export const getDeploymentsColumns = ({
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (configItems.length > 0) {
|
if (configItems.length > 0) {
|
||||||
dropdownItems.push(<Dropdown.Divider key="config-divider" />);
|
dropdownItems.push(<Dropdown.Divider key='config-divider' />);
|
||||||
dropdownItems.push(...configItems);
|
dropdownItems.push(...configItems);
|
||||||
}
|
}
|
||||||
if (!isEnded) {
|
if (!isEnded) {
|
||||||
dropdownItems.push(<Dropdown.Divider key="danger-divider" />);
|
dropdownItems.push(<Dropdown.Divider key='danger-divider' />);
|
||||||
dropdownItems.push(
|
dropdownItems.push(
|
||||||
<Dropdown.Item key="delete" type="danger" onClick={handleDelete} icon={<FaTrash />}>
|
<Dropdown.Item
|
||||||
|
key='delete'
|
||||||
|
type='danger'
|
||||||
|
onClick={handleDelete}
|
||||||
|
icon={<FaTrash />}
|
||||||
|
>
|
||||||
{t('销毁容器')}
|
{t('销毁容器')}
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
);
|
);
|
||||||
@@ -634,14 +664,14 @@ export const getDeploymentsColumns = ({
|
|||||||
const hasDropdown = dropdownItems.length > 0;
|
const hasDropdown = dropdownItems.length > 0;
|
||||||
|
|
||||||
return (
|
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
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
theme={primaryTheme}
|
theme={primaryTheme}
|
||||||
type={primaryType}
|
type={primaryType}
|
||||||
icon={primaryAction.icon}
|
icon={primaryAction.icon}
|
||||||
onClick={primaryAction.onClick}
|
onClick={primaryAction.onClick}
|
||||||
className="px-2 text-xs"
|
className='px-2 text-xs'
|
||||||
disabled={primaryAction.disabled}
|
disabled={primaryAction.disabled}
|
||||||
>
|
>
|
||||||
{primaryAction.text}
|
{primaryAction.text}
|
||||||
@@ -649,16 +679,16 @@ export const getDeploymentsColumns = ({
|
|||||||
|
|
||||||
{hasDropdown && (
|
{hasDropdown && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger="click"
|
trigger='click'
|
||||||
position="bottomRight"
|
position='bottomRight'
|
||||||
render={allActions}
|
render={allActions}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
theme="light"
|
theme='light'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<IconMore />}
|
icon={<IconMore />}
|
||||||
className="px-1"
|
className='px-1'
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const DeploymentsTable = (deploymentsData) => {
|
|||||||
deploymentCount,
|
deploymentCount,
|
||||||
compactMode,
|
compactMode,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
setSelectedKeys,
|
rowSelection,
|
||||||
|
batchOperationsEnabled = true,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handlePageSizeChange,
|
handlePageSizeChange,
|
||||||
handleRow,
|
handleRow,
|
||||||
@@ -95,7 +96,10 @@ const DeploymentsTable = (deploymentsData) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmAction = () => {
|
const handleConfirmAction = () => {
|
||||||
if (selectedDeployment && confirmOperation === 'delete') {
|
if (
|
||||||
|
selectedDeployment &&
|
||||||
|
(confirmOperation === 'delete' || confirmOperation === 'destroy')
|
||||||
|
) {
|
||||||
deleteDeployment(selectedDeployment.id);
|
deleteDeployment(selectedDeployment.id);
|
||||||
}
|
}
|
||||||
setShowConfirmDialog(false);
|
setShowConfirmDialog(false);
|
||||||
@@ -179,11 +183,7 @@ const DeploymentsTable = (deploymentsData) => {
|
|||||||
hidePagination={true}
|
hidePagination={true}
|
||||||
expandAllRows={false}
|
expandAllRows={false}
|
||||||
onRow={handleRow}
|
onRow={handleRow}
|
||||||
rowSelection={{
|
rowSelection={batchOperationsEnabled ? rowSelection : undefined}
|
||||||
onChange: (selectedRowKeys, selectedRows) => {
|
|
||||||
setSelectedKeys(selectedRows);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
empty={
|
empty={
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||||
@@ -235,7 +235,7 @@ const DeploymentsTable = (deploymentsData) => {
|
|||||||
onCancel={() => setShowConfirmDialog(false)}
|
onCancel={() => setShowConfirmDialog(false)}
|
||||||
onConfirm={handleConfirmAction}
|
onConfirm={handleConfirmAction}
|
||||||
title={t('确认操作')}
|
title={t('确认操作')}
|
||||||
type="danger"
|
type='danger'
|
||||||
deployment={selectedDeployment}
|
deployment={selectedDeployment}
|
||||||
operation={confirmOperation}
|
operation={confirmOperation}
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const DeploymentsPage = () => {
|
|||||||
|
|
||||||
// Create deployment modal state
|
// Create deployment modal state
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const batchOperationsEnabled = false;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// Edit state
|
// Edit state
|
||||||
@@ -109,6 +110,7 @@ const DeploymentsPage = () => {
|
|||||||
setEditingDeployment={setEditingDeployment}
|
setEditingDeployment={setEditingDeployment}
|
||||||
setShowEdit={setShowEdit}
|
setShowEdit={setShowEdit}
|
||||||
batchDeleteDeployments={batchDeleteDeployments}
|
batchDeleteDeployments={batchDeleteDeployments}
|
||||||
|
batchOperationsEnabled={batchOperationsEnabled}
|
||||||
compactMode={compactMode}
|
compactMode={compactMode}
|
||||||
setCompactMode={setCompactMode}
|
setCompactMode={setCompactMode}
|
||||||
showCreateModal={showCreateModal}
|
showCreateModal={showCreateModal}
|
||||||
@@ -138,7 +140,10 @@ const DeploymentsPage = () => {
|
|||||||
})}
|
})}
|
||||||
t={deploymentsData.t}
|
t={deploymentsData.t}
|
||||||
>
|
>
|
||||||
<DeploymentsTable {...deploymentsData} />
|
<DeploymentsTable
|
||||||
|
{...deploymentsData}
|
||||||
|
batchOperationsEnabled={batchOperationsEnabled}
|
||||||
|
/>
|
||||||
</CardPro>
|
</CardPro>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -42,19 +42,13 @@ import {
|
|||||||
FaNetworkWired,
|
FaNetworkWired,
|
||||||
FaExclamationTriangle,
|
FaExclamationTriangle,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaMinus
|
FaMinus,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { API, showError, showSuccess } from '../../../../helpers';
|
import { API, showError, showSuccess } from '../../../../helpers';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
const UpdateConfigModal = ({
|
const UpdateConfigModal = ({ visible, onCancel, deployment, onSuccess, t }) => {
|
||||||
visible,
|
|
||||||
onCancel,
|
|
||||||
deployment,
|
|
||||||
onSuccess,
|
|
||||||
t
|
|
||||||
}) => {
|
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [envVars, setEnvVars] = useState([]);
|
const [envVars, setEnvVars] = useState([]);
|
||||||
@@ -79,9 +73,12 @@ const UpdateConfigModal = ({
|
|||||||
|
|
||||||
// Initialize environment variables
|
// Initialize environment variables
|
||||||
const envVarsList = deployment.container_config?.env_variables
|
const envVarsList = deployment.container_config?.env_variables
|
||||||
? Object.entries(deployment.container_config.env_variables).map(([key, value]) => ({
|
? Object.entries(deployment.container_config.env_variables).map(
|
||||||
key, value: String(value)
|
([key, value]) => ({
|
||||||
}))
|
key,
|
||||||
|
value: String(value),
|
||||||
|
}),
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
setEnvVars(envVarsList);
|
setEnvVars(envVarsList);
|
||||||
@@ -91,21 +88,28 @@ const UpdateConfigModal = ({
|
|||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
const formValues = formRef.current ? await formRef.current.validate() : {};
|
const formValues = formRef.current
|
||||||
|
? await formRef.current.validate()
|
||||||
|
: {};
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Prepare the update payload
|
// Prepare the update payload
|
||||||
const payload = {};
|
const payload = {};
|
||||||
|
|
||||||
if (formValues.image_url) payload.image_url = formValues.image_url;
|
if (formValues.image_url) payload.image_url = formValues.image_url;
|
||||||
if (formValues.traffic_port) payload.traffic_port = formValues.traffic_port;
|
if (formValues.traffic_port)
|
||||||
if (formValues.registry_username) payload.registry_username = formValues.registry_username;
|
payload.traffic_port = formValues.traffic_port;
|
||||||
if (formValues.registry_secret) payload.registry_secret = formValues.registry_secret;
|
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;
|
if (formValues.command) payload.command = formValues.command;
|
||||||
|
|
||||||
// Process entrypoint
|
// Process entrypoint
|
||||||
if (formValues.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
|
// Process environment variables
|
||||||
@@ -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) {
|
if (response.data.success) {
|
||||||
showSuccess(t('容器配置更新成功'));
|
showSuccess(t('容器配置更新成功'));
|
||||||
@@ -136,7 +143,11 @@ const UpdateConfigModal = ({
|
|||||||
handleCancel();
|
handleCancel();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('更新配置失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -184,8 +195,8 @@ const UpdateConfigModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaCog className="text-blue-500" />
|
<FaCog className='text-blue-500' />
|
||||||
<span>{t('更新容器配置')}</span>
|
<span>{t('更新容器配置')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -196,77 +207,78 @@ const UpdateConfigModal = ({
|
|||||||
cancelText={t('取消')}
|
cancelText={t('取消')}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
width={700}
|
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 */}
|
{/* Container Info */}
|
||||||
<Card className="border-0 bg-gray-50">
|
<Card className='border-0 bg-gray-50'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div>
|
<div>
|
||||||
<Text strong className="text-base">
|
<Text strong className='text-base'>
|
||||||
{deployment?.container_name}
|
{deployment?.container_name}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="mt-1">
|
<div className='mt-1'>
|
||||||
<Text type="secondary" size="small">
|
<Text type='secondary' size='small'>
|
||||||
ID: {deployment?.id}
|
ID: {deployment?.id}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tag color="blue">{deployment?.status}</Tag>
|
<Tag color='blue'>{deployment?.status}</Tag>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Warning Banner */}
|
{/* Warning Banner */}
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
icon={<FaExclamationTriangle />}
|
icon={<FaExclamationTriangle />}
|
||||||
title={t('重要提醒')}
|
title={t('重要提醒')}
|
||||||
description={
|
description={
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
<p>{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
'更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<p>{t('某些配置更改可能需要几分钟才能生效。')}</p>
|
<p>{t('某些配置更改可能需要几分钟才能生效。')}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form
|
<Form getFormApi={(api) => (formRef.current = api)} layout='vertical'>
|
||||||
getFormApi={(api) => (formRef.current = api)}
|
|
||||||
layout="vertical"
|
|
||||||
>
|
|
||||||
<Collapse defaultActiveKey={['docker']}>
|
<Collapse defaultActiveKey={['docker']}>
|
||||||
{/* Docker Configuration */}
|
{/* Docker Configuration */}
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
header={
|
header={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaDocker className="text-blue-600" />
|
<FaDocker className='text-blue-600' />
|
||||||
<span>{t('Docker 配置')}</span>
|
<span>{t('镜像配置')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="docker"
|
itemKey='docker'
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="image_url"
|
field='image_url'
|
||||||
label={t('镜像地址')}
|
label={t('镜像地址')}
|
||||||
placeholder={t('例如: nginx:latest')}
|
placeholder={t('例如: nginx:latest')}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
message: t('请输入有效的镜像地址')
|
message: t('请输入有效的镜像地址'),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="registry_username"
|
field='registry_username'
|
||||||
label={t('镜像仓库用户名')}
|
label={t('镜像仓库用户名')}
|
||||||
placeholder={t('如果镜像为私有,请填写用户名')}
|
placeholder={t('如果镜像为私有,请填写用户名')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="registry_secret"
|
field='registry_secret'
|
||||||
label={t('镜像仓库密码')}
|
label={t('镜像仓库密码')}
|
||||||
mode="password"
|
mode='password'
|
||||||
placeholder={t('如果镜像为私有,请填写密码或Token')}
|
placeholder={t('如果镜像为私有,请填写密码或Token')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,15 +287,15 @@ const UpdateConfigModal = ({
|
|||||||
{/* Network Configuration */}
|
{/* Network Configuration */}
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
header={
|
header={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaNetworkWired className="text-green-600" />
|
<FaNetworkWired className='text-green-600' />
|
||||||
<span>{t('网络配置')}</span>
|
<span>{t('网络配置')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="network"
|
itemKey='network'
|
||||||
>
|
>
|
||||||
<Form.InputNumber
|
<Form.InputNumber
|
||||||
field="traffic_port"
|
field='traffic_port'
|
||||||
label={t('流量端口')}
|
label={t('流量端口')}
|
||||||
placeholder={t('容器对外暴露的端口')}
|
placeholder={t('容器对外暴露的端口')}
|
||||||
min={1}
|
min={1}
|
||||||
@@ -294,8 +306,8 @@ const UpdateConfigModal = ({
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 65535,
|
max: 65535,
|
||||||
message: t('端口号必须在1-65535之间')
|
message: t('端口号必须在1-65535之间'),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
@@ -303,23 +315,23 @@ const UpdateConfigModal = ({
|
|||||||
{/* Startup Configuration */}
|
{/* Startup Configuration */}
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
header={
|
header={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaTerminal className="text-purple-600" />
|
<FaTerminal className='text-purple-600' />
|
||||||
<span>{t('启动配置')}</span>
|
<span>{t('启动配置')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="startup"
|
itemKey='startup'
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="entrypoint"
|
field='entrypoint'
|
||||||
label={t('启动命令 (Entrypoint)')}
|
label={t('启动命令 (Entrypoint)')}
|
||||||
placeholder={t('例如: /bin/bash -c "python app.py"')}
|
placeholder={t('例如: /bin/bash -c "python app.py"')}
|
||||||
helpText={t('多个命令用空格分隔')}
|
helpText={t('多个命令用空格分隔')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="command"
|
field='command'
|
||||||
label={t('运行命令 (Command)')}
|
label={t('运行命令 (Command)')}
|
||||||
placeholder={t('容器启动后执行的命令')}
|
placeholder={t('容器启动后执行的命令')}
|
||||||
/>
|
/>
|
||||||
@@ -329,32 +341,32 @@ const UpdateConfigModal = ({
|
|||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
header={
|
header={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaKey className="text-orange-600" />
|
<FaKey className='text-orange-600' />
|
||||||
<span>{t('环境变量')}</span>
|
<span>{t('环境变量')}</span>
|
||||||
<Tag size="small">{envVars.length}</Tag>
|
<Tag size='small'>{envVars.length}</Tag>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="env"
|
itemKey='env'
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{/* Regular Environment Variables */}
|
{/* Regular Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className='flex items-center justify-between mb-3'>
|
||||||
<Text strong>{t('普通环境变量')}</Text>
|
<Text strong>{t('普通环境变量')}</Text>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
onClick={addEnvVar}
|
onClick={addEnvVar}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="primary"
|
type='primary'
|
||||||
>
|
>
|
||||||
{t('添加')}
|
{t('添加')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{envVars.map((envVar, index) => (
|
{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
|
<Input
|
||||||
placeholder={t('变量名')}
|
placeholder={t('变量名')}
|
||||||
value={envVar.key}
|
value={envVar.key}
|
||||||
@@ -365,22 +377,24 @@ const UpdateConfigModal = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder={t('变量值')}
|
placeholder={t('变量值')}
|
||||||
value={envVar.value}
|
value={envVar.value}
|
||||||
onChange={(value) => updateEnvVar(index, 'value', value)}
|
onChange={(value) =>
|
||||||
|
updateEnvVar(index, 'value', value)
|
||||||
|
}
|
||||||
style={{ flex: 2 }}
|
style={{ flex: 2 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
icon={<FaMinus />}
|
icon={<FaMinus />}
|
||||||
onClick={() => removeEnvVar(index)}
|
onClick={() => removeEnvVar(index)}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="danger"
|
type='danger'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{envVars.length === 0 && (
|
{envVars.length === 0 && (
|
||||||
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg">
|
<div className='text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg'>
|
||||||
<Text type="secondary">{t('暂无环境变量')}</Text>
|
<Text type='secondary'>{t('暂无环境变量')}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -389,61 +403,67 @@ const UpdateConfigModal = ({
|
|||||||
|
|
||||||
{/* Secret Environment Variables */}
|
{/* Secret Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className='flex items-center justify-between mb-3'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Text strong>{t('机密环境变量')}</Text>
|
<Text strong>{t('机密环境变量')}</Text>
|
||||||
<Tag size="small" type="danger">
|
<Tag size='small' type='danger'>
|
||||||
{t('加密存储')}
|
{t('加密存储')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
onClick={addSecretEnvVar}
|
onClick={addSecretEnvVar}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="danger"
|
type='danger'
|
||||||
>
|
>
|
||||||
{t('添加')}
|
{t('添加')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{secretEnvVars.map((envVar, index) => (
|
{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
|
<Input
|
||||||
placeholder={t('变量名')}
|
placeholder={t('变量名')}
|
||||||
value={envVar.key}
|
value={envVar.key}
|
||||||
onChange={(value) => updateSecretEnvVar(index, 'key', value)}
|
onChange={(value) =>
|
||||||
|
updateSecretEnvVar(index, 'key', value)
|
||||||
|
}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Text>=</Text>
|
<Text>=</Text>
|
||||||
<Input
|
<Input
|
||||||
mode="password"
|
mode='password'
|
||||||
placeholder={t('变量值')}
|
placeholder={t('变量值')}
|
||||||
value={envVar.value}
|
value={envVar.value}
|
||||||
onChange={(value) => updateSecretEnvVar(index, 'value', value)}
|
onChange={(value) =>
|
||||||
|
updateSecretEnvVar(index, 'value', value)
|
||||||
|
}
|
||||||
style={{ flex: 2 }}
|
style={{ flex: 2 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
icon={<FaMinus />}
|
icon={<FaMinus />}
|
||||||
onClick={() => removeSecretEnvVar(index)}
|
onClick={() => removeSecretEnvVar(index)}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="danger"
|
type='danger'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{secretEnvVars.length === 0 && (
|
{secretEnvVars.length === 0 && (
|
||||||
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50">
|
<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>
|
<Text type='secondary'>{t('暂无机密环境变量')}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
type="info"
|
type='info'
|
||||||
title={t('机密环境变量说明')}
|
title={t('机密环境变量说明')}
|
||||||
description={t('机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。')}
|
description={t(
|
||||||
size="small"
|
'机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。',
|
||||||
|
)}
|
||||||
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,16 +472,18 @@ const UpdateConfigModal = ({
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* Final Warning */}
|
{/* Final Warning */}
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
<div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
|
||||||
<div className="flex items-start gap-2">
|
<div className='flex items-start gap-2'>
|
||||||
<FaExclamationTriangle className="text-yellow-600 mt-0.5" />
|
<FaExclamationTriangle className='text-yellow-600 mt-0.5' />
|
||||||
<div>
|
<div>
|
||||||
<Text strong className="text-yellow-800">
|
<Text strong className='text-yellow-800'>
|
||||||
{t('配置更新确认')}
|
{t('配置更新确认')}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="mt-1">
|
<div className='mt-1'>
|
||||||
<Text size="small" className="text-yellow-700">
|
<Text size='small' className='text-yellow-700'>
|
||||||
{t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')}
|
{t(
|
||||||
|
'更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,16 +43,16 @@ import {
|
|||||||
FaLink,
|
FaLink,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { IconRefresh } from '@douyinfe/semi-icons';
|
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 { Text, Title } = Typography;
|
||||||
|
|
||||||
const ViewDetailsModal = ({
|
const ViewDetailsModal = ({ visible, onCancel, deployment, t }) => {
|
||||||
visible,
|
|
||||||
onCancel,
|
|
||||||
deployment,
|
|
||||||
t
|
|
||||||
}) => {
|
|
||||||
const [details, setDetails] = useState(null);
|
const [details, setDetails] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [containers, setContainers] = useState([]);
|
const [containers, setContainers] = useState([]);
|
||||||
@@ -68,7 +68,11 @@ const ViewDetailsModal = ({
|
|||||||
setDetails(response.data.data);
|
setDetails(response.data.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取详情失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -79,12 +83,18 @@ const ViewDetailsModal = ({
|
|||||||
|
|
||||||
setContainersLoading(true);
|
setContainersLoading(true);
|
||||||
try {
|
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) {
|
if (response.data.success) {
|
||||||
setContainers(response.data.data?.containers || []);
|
setContainers(response.data.data?.containers || []);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取容器信息失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取容器信息失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setContainersLoading(false);
|
setContainersLoading(false);
|
||||||
}
|
}
|
||||||
@@ -102,7 +112,7 @@ const ViewDetailsModal = ({
|
|||||||
|
|
||||||
const handleCopyId = () => {
|
const handleCopyId = () => {
|
||||||
navigator.clipboard.writeText(deployment?.id);
|
navigator.clipboard.writeText(deployment?.id);
|
||||||
showSuccess(t('ID已复制到剪贴板'));
|
showSuccess(t('已复制 ID 到剪贴板'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
@@ -112,12 +122,16 @@ const ViewDetailsModal = ({
|
|||||||
|
|
||||||
const getStatusConfig = (status) => {
|
const getStatusConfig = (status) => {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
'running': { color: 'green', text: '运行中', icon: '🟢' },
|
running: { color: 'green', text: '运行中', icon: '🟢' },
|
||||||
'completed': { color: 'green', text: '已完成', icon: '✅' },
|
completed: { color: 'green', text: '已完成', icon: '✅' },
|
||||||
'deployment requested': { color: 'blue', text: '部署请求中', icon: '🔄' },
|
'deployment requested': { color: 'blue', text: '部署请求中', icon: '🔄' },
|
||||||
'termination requested': { color: 'orange', text: '终止请求中', icon: '⏸️' },
|
'termination requested': {
|
||||||
'destroyed': { color: 'red', text: '已销毁', icon: '🔴' },
|
color: 'orange',
|
||||||
'failed': { color: 'red', text: '失败', icon: '❌' }
|
text: '终止请求中',
|
||||||
|
icon: '⏸️',
|
||||||
|
},
|
||||||
|
destroyed: { color: 'red', text: '已销毁', icon: '🔴' },
|
||||||
|
failed: { color: 'red', text: '失败', icon: '❌' },
|
||||||
};
|
};
|
||||||
return statusConfig[status] || { color: 'grey', text: status, icon: '❓' };
|
return statusConfig[status] || { color: 'grey', text: status, icon: '❓' };
|
||||||
};
|
};
|
||||||
@@ -127,149 +141,167 @@ const ViewDetailsModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaInfoCircle className="text-blue-500" />
|
<FaInfoCircle className='text-blue-500' />
|
||||||
<span>{t('容器详情')}</span>
|
<span>{t('容器详情')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
footer={
|
footer={
|
||||||
<div className="flex justify-between">
|
<div className='flex justify-between'>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
loading={loading || containersLoading}
|
loading={loading || containersLoading}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
>
|
>
|
||||||
{t('刷新')}
|
{t('刷新')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel}>
|
<Button onClick={onCancel}>{t('关闭')}</Button>
|
||||||
{t('关闭')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
width={800}
|
width={800}
|
||||||
className="deployment-details-modal"
|
className='deployment-details-modal'
|
||||||
>
|
>
|
||||||
{loading && !details ? (
|
{loading && !details ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className='flex items-center justify-center py-12'>
|
||||||
<Spin size="large" tip={t('加载详情中...')} />
|
<Spin size='large' tip={t('加载详情中...')} />
|
||||||
</div>
|
</div>
|
||||||
) : details ? (
|
) : 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 */}
|
{/* Basic Info */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaServer className="text-blue-500" />
|
<FaServer className='text-blue-500' />
|
||||||
<span>{t('基本信息')}</span>
|
<span>{t('基本信息')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="border-0 shadow-sm"
|
className='border-0 shadow-sm'
|
||||||
>
|
>
|
||||||
<Descriptions data={[
|
<Descriptions
|
||||||
{
|
data={[
|
||||||
key: t('容器名称'),
|
{
|
||||||
value: (
|
key: t('容器名称'),
|
||||||
<div className="flex items-center gap-2">
|
value: (
|
||||||
<Text strong className="text-base">
|
<div className='flex items-center gap-2'>
|
||||||
{details.deployment_name || details.id}
|
<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>
|
</Text>
|
||||||
<Button
|
),
|
||||||
size="small"
|
},
|
||||||
theme="borderless"
|
{
|
||||||
icon={<FaCopy />}
|
key: t('状态'),
|
||||||
onClick={handleCopyId}
|
value: (
|
||||||
className="opacity-70 hover:opacity-100"
|
<div className='flex items-center gap-2'>
|
||||||
/>
|
<span>{statusConfig.icon}</span>
|
||||||
</div>
|
<Tag color={statusConfig.color}>
|
||||||
)
|
{t(statusConfig.text)}
|
||||||
},
|
</Tag>
|
||||||
{
|
</div>
|
||||||
key: t('容器ID'),
|
),
|
||||||
value: (
|
},
|
||||||
<Text type="secondary" className="font-mono text-sm">
|
{
|
||||||
{details.id}
|
key: t('创建时间'),
|
||||||
</Text>
|
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>
|
</Card>
|
||||||
|
|
||||||
{/* Hardware & Performance */}
|
{/* Hardware & Performance */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaChartLine className="text-green-500" />
|
<FaChartLine className='text-green-500' />
|
||||||
<span>{t('硬件与性能')}</span>
|
<span>{t('硬件与性能')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="border-0 shadow-sm"
|
className='border-0 shadow-sm'
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Descriptions data={[
|
<Descriptions
|
||||||
{
|
data={[
|
||||||
key: t('硬件类型'),
|
{
|
||||||
value: (
|
key: t('硬件类型'),
|
||||||
<div className="flex items-center gap-2">
|
value: (
|
||||||
<Tag color="blue">{details.brand_name}</Tag>
|
<div className='flex items-center gap-2'>
|
||||||
<Text strong>{details.hardware_name}</Text>
|
<Tag color='blue'>{details.brand_name}</Tag>
|
||||||
</div>
|
<Text strong>{details.hardware_name}</Text>
|
||||||
)
|
</div>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
key: t('GPU数量'),
|
{
|
||||||
value: (
|
key: t('GPU数量'),
|
||||||
<div className="flex items-center gap-2">
|
value: (
|
||||||
<Badge count={details.total_gpus} theme="solid" type="primary">
|
<div className='flex items-center gap-2'>
|
||||||
<FaServer className="text-purple-500" />
|
<Badge
|
||||||
</Badge>
|
count={details.total_gpus}
|
||||||
<Text>{t('总计')} {details.total_gpus} {t('个GPU')}</Text>
|
theme='solid'
|
||||||
</div>
|
type='primary'
|
||||||
)
|
>
|
||||||
},
|
<FaServer className='text-purple-500' />
|
||||||
{
|
</Badge>
|
||||||
key: t('容器配置'),
|
<Text>
|
||||||
value: (
|
{t('总计')} {details.total_gpus} {t('个GPU')}
|
||||||
<div className="space-y-1">
|
</Text>
|
||||||
<div>{t('每容器GPU数')}: {details.gpus_per_container}</div>
|
</div>
|
||||||
<div>{t('容器总数')}: {details.total_containers}</div>
|
),
|
||||||
</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 */}
|
{/* Progress Bar */}
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Text strong>{t('完成进度')}</Text>
|
<Text strong>{t('完成进度')}</Text>
|
||||||
<Text>{details.completed_percent}%</Text>
|
<Text>{details.completed_percent}%</Text>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={details.completed_percent}
|
percent={details.completed_percent}
|
||||||
status={details.completed_percent === 100 ? 'success' : 'normal'}
|
status={
|
||||||
|
details.completed_percent === 100 ? 'success' : 'normal'
|
||||||
|
}
|
||||||
strokeWidth={8}
|
strokeWidth={8}
|
||||||
showInfo={false}
|
showInfo={false}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
<div className='flex justify-between text-xs text-gray-500'>
|
||||||
<span>{t('已服务')}: {details.compute_minutes_served} {t('分钟')}</span>
|
<span>
|
||||||
<span>{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}</span>
|
{t('已服务')}: {details.compute_minutes_served} {t('分钟')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,54 +311,68 @@ const ViewDetailsModal = ({
|
|||||||
{details.container_config && (
|
{details.container_config && (
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaDocker className="text-blue-600" />
|
<FaDocker className='text-blue-600' />
|
||||||
<span>{t('容器配置')}</span>
|
<span>{t('容器配置')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="border-0 shadow-sm"
|
className='border-0 shadow-sm'
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
<Descriptions data={[
|
<Descriptions
|
||||||
{
|
data={[
|
||||||
key: t('镜像地址'),
|
{
|
||||||
value: (
|
key: t('镜像地址'),
|
||||||
<Text className="font-mono text-sm break-all">
|
value: (
|
||||||
{details.container_config.image_url || 'N/A'}
|
<Text className='font-mono text-sm break-all'>
|
||||||
</Text>
|
{details.container_config.image_url || 'N/A'}
|
||||||
)
|
</Text>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
key: t('流量端口'),
|
{
|
||||||
value: details.container_config.traffic_port || 'N/A'
|
key: t('流量端口'),
|
||||||
},
|
value: details.container_config.traffic_port || 'N/A',
|
||||||
{
|
},
|
||||||
key: t('启动命令'),
|
{
|
||||||
value: (
|
key: t('启动命令'),
|
||||||
<Text className="font-mono text-sm">
|
value: (
|
||||||
{details.container_config.entrypoint ?
|
<Text className='font-mono text-sm'>
|
||||||
details.container_config.entrypoint.join(' ') : 'N/A'
|
{details.container_config.entrypoint
|
||||||
}
|
? details.container_config.entrypoint.join(' ')
|
||||||
</Text>
|
: 'N/A'}
|
||||||
)
|
</Text>
|
||||||
}
|
),
|
||||||
]} />
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
{details.container_config.env_variables &&
|
{details.container_config.env_variables &&
|
||||||
Object.keys(details.container_config.env_variables).length > 0 && (
|
Object.keys(details.container_config.env_variables).length >
|
||||||
<div className="mt-4">
|
0 && (
|
||||||
<Text strong className="block mb-2">{t('环境变量')}:</Text>
|
<div className='mt-4'>
|
||||||
<div className="bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto">
|
<Text strong className='block mb-2'>
|
||||||
{Object.entries(details.container_config.env_variables).map(([key, value]) => (
|
{t('环境变量')}:
|
||||||
<div key={key} className="flex gap-2 text-sm font-mono mb-1">
|
</Text>
|
||||||
<span className="text-blue-600 font-medium">{key}=</span>
|
<div className='bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto'>
|
||||||
<span className="text-gray-700 break-all">{String(value)}</span>
|
{Object.entries(
|
||||||
</div>
|
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>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -334,50 +380,63 @@ const ViewDetailsModal = ({
|
|||||||
{/* Containers List */}
|
{/* Containers List */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaServer className="text-indigo-500" />
|
<FaServer className='text-indigo-500' />
|
||||||
<span>{t('容器实例')}</span>
|
<span>{t('容器实例')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="border-0 shadow-sm"
|
className='border-0 shadow-sm'
|
||||||
>
|
>
|
||||||
{containersLoading ? (
|
{containersLoading ? (
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className='flex items-center justify-center py-6'>
|
||||||
<Spin tip={t('加载容器信息中...')} />
|
<Spin tip={t('加载容器信息中...')} />
|
||||||
</div>
|
</div>
|
||||||
) : containers.length === 0 ? (
|
) : 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) => (
|
{containers.map((ctr) => (
|
||||||
<Card
|
<Card
|
||||||
key={ctr.container_id}
|
key={ctr.container_id}
|
||||||
className="bg-gray-50 border border-gray-100"
|
className='bg-gray-50 border border-gray-100'
|
||||||
bodyStyle={{ padding: '12px 16px' }}
|
bodyStyle={{ padding: '12px 16px' }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
<Text strong className="font-mono text-sm">
|
<Text strong className='font-mono text-sm'>
|
||||||
{ctr.container_id}
|
{ctr.container_id}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('设备')} {ctr.device_id || '--'} · {t('状态')} {ctr.status || '--'}
|
{t('设备')} {ctr.device_id || '--'} · {t('状态')}{' '}
|
||||||
|
{ctr.status || '--'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('创建时间')}: {ctr.created_at ? timestamp2string(ctr.created_at) : '--'}
|
{t('创建时间')}:{' '}
|
||||||
|
{ctr.created_at
|
||||||
|
? timestamp2string(ctr.created_at)
|
||||||
|
: '--'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className='flex flex-col items-end gap-2'>
|
||||||
<Tag color="blue" size="small">
|
<Tag color='blue' size='small'>
|
||||||
{t('GPU/容器')}: {ctr.gpus_per_container ?? '--'}
|
{t('GPU/容器')}: {ctr.gpus_per_container ?? '--'}
|
||||||
</Tag>
|
</Tag>
|
||||||
{ctr.public_url && (
|
{ctr.public_url && (
|
||||||
<Tooltip content={ctr.public_url}>
|
<Tooltip content={ctr.public_url}>
|
||||||
<Button
|
<Button
|
||||||
icon={<FaLink />}
|
icon={<FaLink />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="light"
|
theme='light'
|
||||||
onClick={() => window.open(ctr.public_url, '_blank', 'noopener,noreferrer')}
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
ctr.public_url,
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer',
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t('访问容器')}
|
{t('访问容器')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -387,17 +446,26 @@ const ViewDetailsModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ctr.events && ctr.events.length > 0 && (
|
{ctr.events && ctr.events.length > 0 && (
|
||||||
<div className="mt-3 bg-white rounded-md border border-gray-100 p-3">
|
<div className='mt-3 bg-white rounded-md border border-gray-100 p-3'>
|
||||||
<Text size="small" type="secondary" className="block mb-2">
|
<Text
|
||||||
|
size='small'
|
||||||
|
type='secondary'
|
||||||
|
className='block mb-2'
|
||||||
|
>
|
||||||
{t('最近事件')}
|
{t('最近事件')}
|
||||||
</Text>
|
</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) => (
|
{ctr.events.map((event, index) => (
|
||||||
<div key={`${ctr.container_id}-${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
|
<div
|
||||||
<span className="text-gray-500 min-w-[140px]">
|
key={`${ctr.container_id}-${event.time}-${index}`}
|
||||||
{event.time ? timestamp2string(event.time) : '--'}
|
className='flex gap-3 text-xs font-mono'
|
||||||
|
>
|
||||||
|
<span className='text-gray-500 min-w-[140px]'>
|
||||||
|
{event.time
|
||||||
|
? timestamp2string(event.time)
|
||||||
|
: '--'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-700 break-all flex-1">
|
<span className='text-gray-700 break-all flex-1'>
|
||||||
{event.message || '--'}
|
{event.message || '--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,19 +483,21 @@ const ViewDetailsModal = ({
|
|||||||
{details.locations && details.locations.length > 0 && (
|
{details.locations && details.locations.length > 0 && (
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaMapMarkerAlt className="text-orange-500" />
|
<FaMapMarkerAlt className='text-orange-500' />
|
||||||
<span>{t('部署位置')}</span>
|
<span>{t('部署位置')}</span>
|
||||||
</div>
|
</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) => (
|
{details.locations.map((location) => (
|
||||||
<Tag key={location.id} color="orange" size="large">
|
<Tag key={location.id} color='orange' size='large'>
|
||||||
<div className="flex items-center gap-1">
|
<div className='flex items-center gap-1'>
|
||||||
<span>🌍</span>
|
<span>🌍</span>
|
||||||
<span>{location.name} ({location.iso2})</span>
|
<span>
|
||||||
|
{location.name} ({location.iso2})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
@@ -438,29 +508,41 @@ const ViewDetailsModal = ({
|
|||||||
{/* Cost Information */}
|
{/* Cost Information */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaMoneyBillWave className="text-green-500" />
|
<FaMoneyBillWave className='text-green-500' />
|
||||||
<span>{t('费用信息')}</span>
|
<span>{t('费用信息')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="border-0 shadow-sm"
|
className='border-0 shadow-sm'
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
<div className='flex items-center justify-between p-3 bg-green-50 rounded-lg'>
|
||||||
<Text>{t('已支付金额')}</Text>
|
<Text>{t('已支付金额')}</Text>
|
||||||
<Text strong className="text-lg text-green-600">
|
<Text strong className='text-lg text-green-600'>
|
||||||
${details.amount_paid ? details.amount_paid.toFixed(2) : '0.00'} USDC
|
$
|
||||||
|
{details.amount_paid
|
||||||
|
? details.amount_paid.toFixed(2)
|
||||||
|
: '0.00'}{' '}
|
||||||
|
USDC
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className='grid grid-cols-2 gap-4 text-sm'>
|
||||||
<div className="flex justify-between">
|
<div className='flex justify-between'>
|
||||||
<Text type="secondary">{t('计费开始')}:</Text>
|
<Text type='secondary'>{t('计费开始')}:</Text>
|
||||||
<Text>{details.started_at ? timestamp2string(details.started_at) : 'N/A'}</Text>
|
<Text>
|
||||||
|
{details.started_at
|
||||||
|
? timestamp2string(details.started_at)
|
||||||
|
: 'N/A'}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className='flex justify-between'>
|
||||||
<Text type="secondary">{t('预计结束')}:</Text>
|
<Text type='secondary'>{t('预计结束')}:</Text>
|
||||||
<Text>{details.finished_at ? timestamp2string(details.finished_at) : 'N/A'}</Text>
|
<Text>
|
||||||
|
{details.finished_at
|
||||||
|
? timestamp2string(details.finished_at)
|
||||||
|
: 'N/A'}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,35 +551,37 @@ const ViewDetailsModal = ({
|
|||||||
{/* Time Information */}
|
{/* Time Information */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaClock className="text-purple-500" />
|
<FaClock className='text-purple-500' />
|
||||||
<span>{t('时间信息')}</span>
|
<span>{t('时间信息')}</span>
|
||||||
</div>
|
</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='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Text type="secondary">{t('已运行时间')}:</Text>
|
<Text type='secondary'>{t('已运行时间')}:</Text>
|
||||||
<Text strong>
|
<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>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Text type="secondary">{t('剩余时间')}:</Text>
|
<Text type='secondary'>{t('剩余时间')}:</Text>
|
||||||
<Text strong className="text-orange-600">
|
<Text strong className='text-orange-600'>
|
||||||
{Math.floor(details.compute_minutes_remaining / 60)}h {details.compute_minutes_remaining % 60}m
|
{Math.floor(details.compute_minutes_remaining / 60)}h{' '}
|
||||||
|
{details.compute_minutes_remaining % 60}m
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Text type="secondary">{t('创建时间')}:</Text>
|
<Text type='secondary'>{t('创建时间')}:</Text>
|
||||||
<Text>{timestamp2string(details.created_at)}</Text>
|
<Text>{timestamp2string(details.created_at)}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Text type="secondary">{t('最后更新')}:</Text>
|
<Text type='secondary'>{t('最后更新')}:</Text>
|
||||||
<Text>{timestamp2string(details.updated_at)}</Text>
|
<Text>{timestamp2string(details.updated_at)}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,18 +44,19 @@ import {
|
|||||||
FaLink,
|
FaLink,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { IconRefresh, IconDownload } from '@douyinfe/semi-icons';
|
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 { Text } = Typography;
|
||||||
|
|
||||||
const ALL_CONTAINERS = '__all__';
|
const ALL_CONTAINERS = '__all__';
|
||||||
|
|
||||||
const ViewLogsModal = ({
|
const ViewLogsModal = ({ visible, onCancel, deployment, t }) => {
|
||||||
visible,
|
|
||||||
onCancel,
|
|
||||||
deployment,
|
|
||||||
t
|
|
||||||
}) => {
|
|
||||||
const [logLines, setLogLines] = useState([]);
|
const [logLines, setLogLines] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
@@ -63,7 +64,8 @@ const ViewLogsModal = ({
|
|||||||
const [following, setFollowing] = useState(false);
|
const [following, setFollowing] = useState(false);
|
||||||
const [containers, setContainers] = useState([]);
|
const [containers, setContainers] = useState([]);
|
||||||
const [containersLoading, setContainersLoading] = useState(false);
|
const [containersLoading, setContainersLoading] = useState(false);
|
||||||
const [selectedContainerId, setSelectedContainerId] = useState(ALL_CONTAINERS);
|
const [selectedContainerId, setSelectedContainerId] =
|
||||||
|
useState(ALL_CONTAINERS);
|
||||||
const [containerDetails, setContainerDetails] = useState(null);
|
const [containerDetails, setContainerDetails] = useState(null);
|
||||||
const [containerDetailsLoading, setContainerDetailsLoading] = useState(false);
|
const [containerDetailsLoading, setContainerDetailsLoading] = useState(false);
|
||||||
const [streamFilter, setStreamFilter] = useState('stdout');
|
const [streamFilter, setStreamFilter] = useState('stdout');
|
||||||
@@ -100,7 +102,10 @@ const ViewLogsModal = ({
|
|||||||
const fetchLogs = async (containerIdOverride = undefined) => {
|
const fetchLogs = async (containerIdOverride = undefined) => {
|
||||||
if (!deployment?.id) return;
|
if (!deployment?.id) return;
|
||||||
|
|
||||||
const containerId = typeof containerIdOverride === 'string' ? containerIdOverride : selectedContainerId;
|
const containerId =
|
||||||
|
typeof containerIdOverride === 'string'
|
||||||
|
? containerIdOverride
|
||||||
|
: selectedContainerId;
|
||||||
|
|
||||||
if (!containerId || containerId === ALL_CONTAINERS) {
|
if (!containerId || containerId === ALL_CONTAINERS) {
|
||||||
setLogLines([]);
|
setLogLines([]);
|
||||||
@@ -120,10 +125,13 @@ const ViewLogsModal = ({
|
|||||||
}
|
}
|
||||||
if (following) params.append('follow', 'true');
|
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) {
|
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 normalized = rawContent.replace(/\r\n?/g, '\n');
|
||||||
const lines = normalized ? normalized.split('\n') : [];
|
const lines = normalized ? normalized.split('\n') : [];
|
||||||
|
|
||||||
@@ -133,7 +141,11 @@ const ViewLogsModal = ({
|
|||||||
setTimeout(scrollToBottom, 100);
|
setTimeout(scrollToBottom, 100);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取日志失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -144,14 +156,19 @@ const ViewLogsModal = ({
|
|||||||
|
|
||||||
setContainersLoading(true);
|
setContainersLoading(true);
|
||||||
try {
|
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) {
|
if (response.data.success) {
|
||||||
const list = response.data.data?.containers || [];
|
const list = response.data.data?.containers || [];
|
||||||
setContainers(list);
|
setContainers(list);
|
||||||
|
|
||||||
setSelectedContainerId((current) => {
|
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;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +180,11 @@ const ViewLogsModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取容器列表失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取容器列表失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setContainersLoading(false);
|
setContainersLoading(false);
|
||||||
}
|
}
|
||||||
@@ -177,13 +198,19 @@ const ViewLogsModal = ({
|
|||||||
|
|
||||||
setContainerDetailsLoading(true);
|
setContainerDetailsLoading(true);
|
||||||
try {
|
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) {
|
if (response.data.success) {
|
||||||
setContainerDetails(response.data.data || null);
|
setContainerDetails(response.data.data || null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取容器详情失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取容器详情失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setContainerDetailsLoading(false);
|
setContainerDetailsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -205,13 +232,14 @@ const ViewLogsModal = ({
|
|||||||
const renderContainerStatusTag = (status) => {
|
const renderContainerStatusTag = (status) => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return (
|
return (
|
||||||
<Tag color="grey" size="small">
|
<Tag color='grey' size='small'>
|
||||||
{t('未知状态')}
|
{t('未知状态')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : '';
|
const normalized =
|
||||||
|
typeof status === 'string' ? status.trim().toLowerCase() : '';
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
running: { color: 'green', label: '运行中' },
|
running: { color: 'green', label: '运行中' },
|
||||||
pending: { color: 'orange', label: '准备中' },
|
pending: { color: 'orange', label: '准备中' },
|
||||||
@@ -225,15 +253,16 @@ const ViewLogsModal = ({
|
|||||||
const config = statusMap[normalized] || { color: 'grey', label: status };
|
const config = statusMap[normalized] || { color: 'grey', label: status };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag color={config.color} size="small">
|
<Tag color={config.color} size='small'>
|
||||||
{t(config.label)}
|
{t(config.label)}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentContainer = selectedContainerId !== ALL_CONTAINERS
|
const currentContainer =
|
||||||
? containers.find((ctr) => ctr.container_id === selectedContainerId)
|
selectedContainerId !== ALL_CONTAINERS
|
||||||
: null;
|
? containers.find((ctr) => ctr.container_id === selectedContainerId)
|
||||||
|
: null;
|
||||||
|
|
||||||
const refreshLogs = () => {
|
const refreshLogs = () => {
|
||||||
if (selectedContainerId && selectedContainerId !== ALL_CONTAINERS) {
|
if (selectedContainerId && selectedContainerId !== ALL_CONTAINERS) {
|
||||||
@@ -254,9 +283,10 @@ const ViewLogsModal = ({
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
const safeContainerId = selectedContainerId && selectedContainerId !== ALL_CONTAINERS
|
const safeContainerId =
|
||||||
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
|
selectedContainerId && selectedContainerId !== ALL_CONTAINERS
|
||||||
: '';
|
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
|
||||||
|
: '';
|
||||||
const fileName = safeContainerId
|
const fileName = safeContainerId
|
||||||
? `deployment-${deployment.id}-container-${safeContainerId}-logs.txt`
|
? `deployment-${deployment.id}-container-${safeContainerId}-logs.txt`
|
||||||
: `deployment-${deployment.id}-logs.txt`;
|
: `deployment-${deployment.id}-logs.txt`;
|
||||||
@@ -346,14 +376,15 @@ const ViewLogsModal = ({
|
|||||||
// Filter logs based on search term
|
// Filter logs based on search term
|
||||||
const filteredLogs = logLines
|
const filteredLogs = logLines
|
||||||
.map((line) => line ?? '')
|
.map((line) => line ?? '')
|
||||||
.filter((line) =>
|
.filter(
|
||||||
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
|
(line) =>
|
||||||
|
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderLogEntry = (line, index) => (
|
const renderLogEntry = (line, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${index}-${line.slice(0, 20)}`}
|
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}
|
{line}
|
||||||
</div>
|
</div>
|
||||||
@@ -362,10 +393,10 @@ const ViewLogsModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaTerminal className="text-blue-500" />
|
<FaTerminal className='text-blue-500' />
|
||||||
<span>{t('容器日志')}</span>
|
<span>{t('容器日志')}</span>
|
||||||
<Text type="secondary" size="small">
|
<Text type='secondary' size='small'>
|
||||||
- {deployment?.container_name || deployment?.id}
|
- {deployment?.container_name || deployment?.id}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,13 +406,13 @@ const ViewLogsModal = ({
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={1000}
|
width={1000}
|
||||||
height={700}
|
height={700}
|
||||||
className="logs-modal"
|
className='logs-modal'
|
||||||
style={{ top: 20 }}
|
style={{ top: 20 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full max-h-[600px]">
|
<div className='flex flex-col h-full max-h-[600px]'>
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<Card className="mb-4 border-0 shadow-sm">
|
<Card className='mb-4 border-0 shadow-sm'>
|
||||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
<div className='flex items-center justify-between flex-wrap gap-3'>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Select
|
<Select
|
||||||
prefix={<FaServer />}
|
prefix={<FaServer />}
|
||||||
@@ -389,7 +420,7 @@ const ViewLogsModal = ({
|
|||||||
value={selectedContainerId}
|
value={selectedContainerId}
|
||||||
onChange={handleContainerChange}
|
onChange={handleContainerChange}
|
||||||
style={{ width: 240 }}
|
style={{ width: 240 }}
|
||||||
size="small"
|
size='small'
|
||||||
loading={containersLoading}
|
loading={containersLoading}
|
||||||
dropdownStyle={{ maxHeight: 320, overflowY: 'auto' }}
|
dropdownStyle={{ maxHeight: 320, overflowY: 'auto' }}
|
||||||
>
|
>
|
||||||
@@ -397,10 +428,15 @@ const ViewLogsModal = ({
|
|||||||
{t('全部容器')}
|
{t('全部容器')}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
{containers.map((ctr) => (
|
{containers.map((ctr) => (
|
||||||
<Select.Option key={ctr.container_id} value={ctr.container_id}>
|
<Select.Option
|
||||||
<div className="flex flex-col">
|
key={ctr.container_id}
|
||||||
<span className="font-mono text-xs">{ctr.container_id}</span>
|
value={ctr.container_id}
|
||||||
<span className="text-xs text-gray-500">
|
>
|
||||||
|
<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.brand_name || 'IO.NET'}
|
||||||
{ctr.hardware ? ` · ${ctr.hardware}` : ''}
|
{ctr.hardware ? ` · ${ctr.hardware}` : ''}
|
||||||
</span>
|
</span>
|
||||||
@@ -415,40 +451,40 @@ const ViewLogsModal = ({
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={setSearchTerm}
|
onChange={setSearchTerm}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Space align="center" className="ml-2">
|
<Space align='center' className='ml-2'>
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('日志流')}
|
{t('日志流')}
|
||||||
</Text>
|
</Text>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
type="button"
|
type='button'
|
||||||
size="small"
|
size='small'
|
||||||
value={streamFilter}
|
value={streamFilter}
|
||||||
onChange={handleStreamChange}
|
onChange={handleStreamChange}
|
||||||
>
|
>
|
||||||
<Radio value="stdout">STDOUT</Radio>
|
<Radio value='stdout'>STDOUT</Radio>
|
||||||
<Radio value="stderr">STDERR</Radio>
|
<Radio value='stderr'>STDERR</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={autoRefresh}
|
checked={autoRefresh}
|
||||||
onChange={setAutoRefresh}
|
onChange={setAutoRefresh}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
<Text size="small">{t('自动刷新')}</Text>
|
<Text size='small'>{t('自动刷新')}</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={following}
|
checked={following}
|
||||||
onChange={setFollowing}
|
onChange={setFollowing}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
<Text size="small">{t('跟随日志')}</Text>
|
<Text size='small'>{t('跟随日志')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
@@ -458,8 +494,8 @@ const ViewLogsModal = ({
|
|||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
onClick={refreshLogs}
|
onClick={refreshLogs}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@@ -467,8 +503,8 @@ const ViewLogsModal = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<FaCopy />}
|
icon={<FaCopy />}
|
||||||
onClick={copyAllLogs}
|
onClick={copyAllLogs}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
disabled={logLines.length === 0}
|
disabled={logLines.length === 0}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -477,8 +513,8 @@ const ViewLogsModal = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<IconDownload />}
|
icon={<IconDownload />}
|
||||||
onClick={downloadLogs}
|
onClick={downloadLogs}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
disabled={logLines.length === 0}
|
disabled={logLines.length === 0}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -486,43 +522,47 @@ const ViewLogsModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Info */}
|
{/* Status Info */}
|
||||||
<Divider margin="12px" />
|
<Divider margin='12px' />
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Space size="large">
|
<Space size='large'>
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('共 {{count}} 条日志', { count: logLines.length })}
|
{t('共 {{count}} 条日志', { count: logLines.length })}
|
||||||
</Text>
|
</Text>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('(筛选后显示 {{count}} 条)', { count: filteredLogs.length })}
|
{t('(筛选后显示 {{count}} 条)', {
|
||||||
|
count: filteredLogs.length,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{autoRefresh && (
|
{autoRefresh && (
|
||||||
<Tag color="green" size="small">
|
<Tag color='green' size='small'>
|
||||||
<FaClock className="mr-1" />
|
<FaClock className='mr-1' />
|
||||||
{t('自动刷新中')}
|
{t('自动刷新中')}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('状态')}: {deployment?.status || 'unknown'}
|
{t('状态')}: {deployment?.status || 'unknown'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedContainerId !== ALL_CONTAINERS && (
|
{selectedContainerId !== ALL_CONTAINERS && (
|
||||||
<>
|
<>
|
||||||
<Divider margin="12px" />
|
<Divider margin='12px' />
|
||||||
<div className="flex flex-col gap-3">
|
<div className='flex flex-col gap-3'>
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className='flex items-center justify-between flex-wrap gap-2'>
|
||||||
<Space>
|
<Space>
|
||||||
<Tag color="blue" size="small">
|
<Tag color='blue' size='small'>
|
||||||
{t('容器')}
|
{t('容器')}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Text className="font-mono text-xs">
|
<Text className='font-mono text-xs'>
|
||||||
{selectedContainerId}
|
{selectedContainerId}
|
||||||
</Text>
|
</Text>
|
||||||
{renderContainerStatusTag(containerDetails?.status || currentContainer?.status)}
|
{renderContainerStatusTag(
|
||||||
|
containerDetails?.status || currentContainer?.status,
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
@@ -530,9 +570,11 @@ const ViewLogsModal = ({
|
|||||||
<Tooltip content={containerDetails.public_url}>
|
<Tooltip content={containerDetails.public_url}>
|
||||||
<Button
|
<Button
|
||||||
icon={<FaLink />}
|
icon={<FaLink />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
onClick={() => window.open(containerDetails.public_url, '_blank')}
|
onClick={() =>
|
||||||
|
window.open(containerDetails.public_url, '_blank')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -540,8 +582,8 @@ const ViewLogsModal = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
onClick={refreshContainerDetails}
|
onClick={refreshContainerDetails}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
loading={containerDetailsLoading}
|
loading={containerDetailsLoading}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -549,27 +591,36 @@ const ViewLogsModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{containerDetailsLoading ? (
|
{containerDetailsLoading ? (
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className='flex items-center justify-center py-6'>
|
||||||
<Spin tip={t('加载容器详情中...')} />
|
<Spin tip={t('加载容器详情中...')} />
|
||||||
</div>
|
</div>
|
||||||
) : containerDetails ? (
|
) : containerDetails ? (
|
||||||
<div className="grid gap-4 md:grid-cols-2 text-sm">
|
<div className='grid gap-4 md:grid-cols-2 text-sm'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaInfoCircle className="text-blue-500" />
|
<FaInfoCircle className='text-blue-500' />
|
||||||
<Text type="secondary">{t('硬件')}</Text>
|
<Text type='secondary'>{t('硬件')}</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{containerDetails?.brand_name || currentContainer?.brand_name || t('未知品牌')}
|
{containerDetails?.brand_name ||
|
||||||
{(containerDetails?.hardware || currentContainer?.hardware) ? ` · ${containerDetails?.hardware || currentContainer?.hardware}` : ''}
|
currentContainer?.brand_name ||
|
||||||
|
t('未知品牌')}
|
||||||
|
{containerDetails?.hardware ||
|
||||||
|
currentContainer?.hardware
|
||||||
|
? ` · ${containerDetails?.hardware || currentContainer?.hardware}`
|
||||||
|
: ''}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaServer className="text-purple-500" />
|
<FaServer className='text-purple-500' />
|
||||||
<Text type="secondary">{t('GPU/容器')}</Text>
|
<Text type='secondary'>{t('GPU/容器')}</Text>
|
||||||
<Text>{containerDetails?.gpus_per_container ?? currentContainer?.gpus_per_container ?? 0}</Text>
|
<Text>
|
||||||
|
{containerDetails?.gpus_per_container ??
|
||||||
|
currentContainer?.gpus_per_container ??
|
||||||
|
0}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaClock className="text-orange-500" />
|
<FaClock className='text-orange-500' />
|
||||||
<Text type="secondary">{t('创建时间')}</Text>
|
<Text type='secondary'>{t('创建时间')}</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{containerDetails?.created_at
|
{containerDetails?.created_at
|
||||||
? timestamp2string(containerDetails.created_at)
|
? timestamp2string(containerDetails.created_at)
|
||||||
@@ -578,51 +629,64 @@ const ViewLogsModal = ({
|
|||||||
: t('未知')}
|
: t('未知')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<FaInfoCircle className="text-green-500" />
|
<FaInfoCircle className='text-green-500' />
|
||||||
<Text type="secondary">{t('运行时长')}</Text>
|
<Text type='secondary'>{t('运行时长')}</Text>
|
||||||
<Text>{containerDetails?.uptime_percent ?? currentContainer?.uptime_percent ?? 0}%</Text>
|
<Text>
|
||||||
|
{containerDetails?.uptime_percent ??
|
||||||
|
currentContainer?.uptime_percent ??
|
||||||
|
0}
|
||||||
|
%
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
{t('暂无容器详情')}
|
{t('暂无容器详情')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{containerDetails?.events && containerDetails.events.length > 0 && (
|
{containerDetails?.events &&
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
containerDetails.events.length > 0 && (
|
||||||
<Text size="small" type="secondary">
|
<div className='bg-gray-50 rounded-lg p-3'>
|
||||||
{t('最近事件')}
|
<Text size='small' type='secondary'>
|
||||||
</Text>
|
{t('最近事件')}
|
||||||
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto">
|
</Text>
|
||||||
{containerDetails.events.slice(0, 5).map((event, index) => (
|
<div className='mt-2 space-y-2 max-h-32 overflow-y-auto'>
|
||||||
<div key={`${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
|
{containerDetails.events
|
||||||
<span className="text-gray-500">
|
.slice(0, 5)
|
||||||
{event.time ? timestamp2string(event.time) : '--'}
|
.map((event, index) => (
|
||||||
</span>
|
<div
|
||||||
<span className="text-gray-700 break-all flex-1">
|
key={`${event.time}-${index}`}
|
||||||
{event.message}
|
className='flex gap-3 text-xs font-mono'
|
||||||
</span>
|
>
|
||||||
</div>
|
<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>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Log Content */}
|
{/* Log Content */}
|
||||||
<div className="flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden">
|
<div className='flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden'>
|
||||||
<div
|
<div
|
||||||
ref={logContainerRef}
|
ref={logContainerRef}
|
||||||
className="flex-1 overflow-y-auto bg-white"
|
className='flex-1 overflow-y-auto bg-white'
|
||||||
style={{ maxHeight: '400px' }}
|
style={{ maxHeight: '400px' }}
|
||||||
>
|
>
|
||||||
{loading && logLines.length === 0 ? (
|
{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('加载日志中...')} />
|
<Spin tip={t('加载日志中...')} />
|
||||||
</div>
|
</div>
|
||||||
) : filteredLogs.length === 0 ? (
|
) : filteredLogs.length === 0 ? (
|
||||||
@@ -642,12 +706,11 @@ const ViewLogsModal = ({
|
|||||||
|
|
||||||
{/* Footer status */}
|
{/* Footer status */}
|
||||||
{logLines.length > 0 && (
|
{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>
|
<span>
|
||||||
{following ? t('正在跟随最新日志') : t('日志已加载')}
|
{t('最后更新')}:{' '}
|
||||||
</span>
|
{lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
|
||||||
<span>
|
|
||||||
{t('最后更新')}: {lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,6 +25,56 @@ import { API } from '../../helpers';
|
|||||||
const sidebarEventTarget = new EventTarget();
|
const sidebarEventTarget = new EventTarget();
|
||||||
const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
|
const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
|
||||||
|
|
||||||
|
export const DEFAULT_ADMIN_CONFIG = {
|
||||||
|
chat: {
|
||||||
|
enabled: true,
|
||||||
|
playground: true,
|
||||||
|
chat: true,
|
||||||
|
},
|
||||||
|
console: {
|
||||||
|
enabled: true,
|
||||||
|
detail: true,
|
||||||
|
token: true,
|
||||||
|
log: true,
|
||||||
|
midjourney: true,
|
||||||
|
task: true,
|
||||||
|
},
|
||||||
|
personal: {
|
||||||
|
enabled: true,
|
||||||
|
topup: true,
|
||||||
|
personal: true,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
enabled: true,
|
||||||
|
channel: true,
|
||||||
|
models: true,
|
||||||
|
deployment: true,
|
||||||
|
redemption: true,
|
||||||
|
user: true,
|
||||||
|
setting: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepClone = (value) => JSON.parse(JSON.stringify(value));
|
||||||
|
|
||||||
|
export const mergeAdminConfig = (savedConfig) => {
|
||||||
|
const merged = deepClone(DEFAULT_ADMIN_CONFIG);
|
||||||
|
if (!savedConfig || typeof savedConfig !== 'object') return merged;
|
||||||
|
|
||||||
|
for (const [sectionKey, sectionConfig] of Object.entries(savedConfig)) {
|
||||||
|
if (!sectionConfig || typeof sectionConfig !== 'object') continue;
|
||||||
|
|
||||||
|
if (!merged[sectionKey]) {
|
||||||
|
merged[sectionKey] = { ...sectionConfig };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged[sectionKey] = { ...merged[sectionKey], ...sectionConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
export const useSidebar = () => {
|
export const useSidebar = () => {
|
||||||
const [statusState] = useContext(StatusContext);
|
const [statusState] = useContext(StatusContext);
|
||||||
const [userConfig, setUserConfig] = useState(null);
|
const [userConfig, setUserConfig] = useState(null);
|
||||||
@@ -37,48 +87,17 @@ export const useSidebar = () => {
|
|||||||
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
|
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认配置
|
|
||||||
const defaultAdminConfig = {
|
|
||||||
chat: {
|
|
||||||
enabled: true,
|
|
||||||
playground: true,
|
|
||||||
chat: true,
|
|
||||||
},
|
|
||||||
console: {
|
|
||||||
enabled: true,
|
|
||||||
detail: true,
|
|
||||||
token: true,
|
|
||||||
log: true,
|
|
||||||
midjourney: true,
|
|
||||||
task: true,
|
|
||||||
},
|
|
||||||
personal: {
|
|
||||||
enabled: true,
|
|
||||||
topup: true,
|
|
||||||
personal: true,
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
enabled: true,
|
|
||||||
channel: true,
|
|
||||||
models: true,
|
|
||||||
deployment: true,
|
|
||||||
redemption: true,
|
|
||||||
user: true,
|
|
||||||
setting: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取管理员配置
|
// 获取管理员配置
|
||||||
const adminConfig = useMemo(() => {
|
const adminConfig = useMemo(() => {
|
||||||
if (statusState?.status?.SidebarModulesAdmin) {
|
if (statusState?.status?.SidebarModulesAdmin) {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(statusState.status.SidebarModulesAdmin);
|
const config = JSON.parse(statusState.status.SidebarModulesAdmin);
|
||||||
return config;
|
return mergeAdminConfig(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return defaultAdminConfig;
|
return mergeAdminConfig(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultAdminConfig;
|
return mergeAdminConfig(null);
|
||||||
}, [statusState?.status?.SidebarModulesAdmin]);
|
}, [statusState?.status?.SidebarModulesAdmin]);
|
||||||
|
|
||||||
// 加载用户配置的通用方法
|
// 加载用户配置的通用方法
|
||||||
|
|||||||
@@ -39,10 +39,13 @@ export const useDeploymentResources = () => {
|
|||||||
setLoadingHardware(true);
|
setLoadingHardware(true);
|
||||||
const response = await API.get('/api/deployments/hardware-types');
|
const response = await API.get('/api/deployments/hardware-types');
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const { hardware_types: hardwareList = [], total_available } = response.data.data || {};
|
const { hardware_types: hardwareList = [], total_available } =
|
||||||
|
response.data.data || {};
|
||||||
const normalizedHardware = hardwareList.map((hardware) => {
|
const normalizedHardware = hardwareList.map((hardware) => {
|
||||||
const availableCountValue = Number(hardware.available_count);
|
const availableCountValue = Number(hardware.available_count);
|
||||||
const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue;
|
const availableCount = Number.isNaN(availableCountValue)
|
||||||
|
? 0
|
||||||
|
: availableCountValue;
|
||||||
const availableBool =
|
const availableBool =
|
||||||
typeof hardware.available === 'boolean'
|
typeof hardware.available === 'boolean'
|
||||||
? hardware.available
|
? hardware.available
|
||||||
@@ -57,7 +60,9 @@ export const useDeploymentResources = () => {
|
|||||||
|
|
||||||
const providedTotal = Number(total_available);
|
const providedTotal = Number(total_available);
|
||||||
const fallbackTotal = normalizedHardware.reduce(
|
const fallbackTotal = normalizedHardware.reduce(
|
||||||
(acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
|
(acc, item) =>
|
||||||
|
acc +
|
||||||
|
(Number.isNaN(item.available_count) ? 0 : item.available_count),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const hasProvidedTotal =
|
const hasProvidedTotal =
|
||||||
@@ -85,37 +90,64 @@ export const useDeploymentResources = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchLocations = useCallback(async () => {
|
const fetchLocations = useCallback(async (hardwareId, gpuCount = 1) => {
|
||||||
|
if (!hardwareId) {
|
||||||
|
setLocations([]);
|
||||||
|
setLocationsTotalAvailable(0);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingLocations(true);
|
setLoadingLocations(true);
|
||||||
const response = await API.get('/api/deployments/locations');
|
const response = await API.get(
|
||||||
|
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
|
||||||
|
);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const { locations: locationsList = [], total } = response.data.data || {};
|
const replicas = response.data.data?.replicas || [];
|
||||||
const normalizedLocations = locationsList.map((location) => {
|
const nextLocationsMap = new Map();
|
||||||
const iso2 = (location.iso2 || '').toString().toUpperCase();
|
replicas.forEach((replica) => {
|
||||||
const availableValue = Number(location.available);
|
const rawId = replica?.location_id ?? replica?.location?.id;
|
||||||
const available = Number.isNaN(availableValue) ? 0 : availableValue;
|
if (rawId === null || rawId === undefined) return;
|
||||||
|
|
||||||
return {
|
const mapKey = String(rawId);
|
||||||
...location,
|
if (nextLocationsMap.has(mapKey)) return;
|
||||||
|
|
||||||
|
const rawIso2 =
|
||||||
|
replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2;
|
||||||
|
const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : '';
|
||||||
|
const name =
|
||||||
|
replica?.location_name ??
|
||||||
|
replica?.location?.name ??
|
||||||
|
replica?.name ??
|
||||||
|
String(rawId);
|
||||||
|
|
||||||
|
nextLocationsMap.set(mapKey, {
|
||||||
|
id: rawId,
|
||||||
|
name: String(name),
|
||||||
iso2,
|
iso2,
|
||||||
available,
|
region:
|
||||||
};
|
replica?.region ??
|
||||||
|
replica?.location_region ??
|
||||||
|
replica?.location?.region,
|
||||||
|
country:
|
||||||
|
replica?.country ??
|
||||||
|
replica?.location_country ??
|
||||||
|
replica?.location?.country,
|
||||||
|
code:
|
||||||
|
replica?.code ??
|
||||||
|
replica?.location_code ??
|
||||||
|
replica?.location?.code,
|
||||||
|
available: Number(replica?.available_count) || 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const providedTotal = Number(total);
|
|
||||||
const fallbackTotal = normalizedLocations.reduce(
|
|
||||||
(acc, item) => acc + (Number.isNaN(item.available) ? 0 : item.available),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const hasProvidedTotal =
|
|
||||||
total !== undefined &&
|
|
||||||
total !== null &&
|
|
||||||
total !== '' &&
|
|
||||||
!Number.isNaN(providedTotal);
|
|
||||||
|
|
||||||
|
const normalizedLocations = Array.from(nextLocationsMap.values());
|
||||||
setLocations(normalizedLocations);
|
setLocations(normalizedLocations);
|
||||||
setLocationsTotalAvailable(
|
setLocationsTotalAvailable(
|
||||||
hasProvidedTotal ? providedTotal : fallbackTotal,
|
normalizedLocations.reduce(
|
||||||
|
(acc, item) => acc + (item.available || 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return normalizedLocations;
|
return normalizedLocations;
|
||||||
} else {
|
} else {
|
||||||
@@ -132,34 +164,37 @@ export const useDeploymentResources = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchAvailableReplicas = useCallback(async (hardwareId, gpuCount = 1) => {
|
const fetchAvailableReplicas = useCallback(
|
||||||
if (!hardwareId) {
|
async (hardwareId, gpuCount = 1) => {
|
||||||
setAvailableReplicas([]);
|
if (!hardwareId) {
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadingReplicas(true);
|
|
||||||
const response = await API.get(
|
|
||||||
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`
|
|
||||||
);
|
|
||||||
if (response.data.success) {
|
|
||||||
const replicas = response.data.data.replicas || [];
|
|
||||||
setAvailableReplicas(replicas);
|
|
||||||
return replicas;
|
|
||||||
} else {
|
|
||||||
showError('获取可用资源失败: ' + response.data.message);
|
|
||||||
setAvailableReplicas([]);
|
setAvailableReplicas([]);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Load available replicas error:', error);
|
try {
|
||||||
setAvailableReplicas([]);
|
setLoadingReplicas(true);
|
||||||
return [];
|
const response = await API.get(
|
||||||
} finally {
|
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
|
||||||
setLoadingReplicas(false);
|
);
|
||||||
}
|
if (response.data.success) {
|
||||||
}, []);
|
const replicas = response.data.data.replicas || [];
|
||||||
|
setAvailableReplicas(replicas);
|
||||||
|
return replicas;
|
||||||
|
} else {
|
||||||
|
showError('获取可用资源失败: ' + response.data.message);
|
||||||
|
setAvailableReplicas([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load available replicas error:', error);
|
||||||
|
setAvailableReplicas([]);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setLoadingReplicas(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const calculatePrice = useCallback(async (params) => {
|
const calculatePrice = useCallback(async (params) => {
|
||||||
const {
|
const {
|
||||||
@@ -167,10 +202,16 @@ export const useDeploymentResources = () => {
|
|||||||
hardwareId,
|
hardwareId,
|
||||||
gpusPerContainer,
|
gpusPerContainer,
|
||||||
durationHours,
|
durationHours,
|
||||||
replicaCount
|
replicaCount,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!locationIds?.length || !hardwareId || !gpusPerContainer || !durationHours || !replicaCount) {
|
if (
|
||||||
|
!locationIds?.length ||
|
||||||
|
!hardwareId ||
|
||||||
|
!gpusPerContainer ||
|
||||||
|
!durationHours ||
|
||||||
|
!replicaCount
|
||||||
|
) {
|
||||||
setPriceEstimation(null);
|
setPriceEstimation(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -185,7 +226,10 @@ export const useDeploymentResources = () => {
|
|||||||
replica_count: replicaCount,
|
replica_count: replicaCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await API.post('/api/deployments/price-estimation', requestData);
|
const response = await API.post(
|
||||||
|
'/api/deployments/price-estimation',
|
||||||
|
requestData,
|
||||||
|
);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const estimation = response.data.data;
|
const estimation = response.data.data;
|
||||||
setPriceEstimation(estimation);
|
setPriceEstimation(estimation);
|
||||||
@@ -208,7 +252,9 @@ export const useDeploymentResources = () => {
|
|||||||
if (!name?.trim()) return false;
|
if (!name?.trim()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await API.get(`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`);
|
const response = await API.get(
|
||||||
|
`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`,
|
||||||
|
);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
return response.data.data.available;
|
return response.data.data.available;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
@@ -26,6 +26,7 @@ import { useTableCompactMode } from '../common/useTableCompactMode';
|
|||||||
export const useDeploymentsData = () => {
|
export const useDeploymentsData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('deployments');
|
const [compactMode, setCompactMode] = useTableCompactMode('deployments');
|
||||||
|
const requestSeq = useRef(0);
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [deployments, setDeployments] = useState([]);
|
const [deployments, setDeployments] = useState([]);
|
||||||
@@ -34,6 +35,7 @@ export const useDeploymentsData = () => {
|
|||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [deploymentCount, setDeploymentCount] = useState(0);
|
const [deploymentCount, setDeploymentCount] = useState(0);
|
||||||
|
const [query, setQuery] = useState({ keyword: '', status: '' });
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
@@ -80,18 +82,12 @@ export const useDeploymentsData = () => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set deployment format with key field
|
const normalizeQuery = (terms) => {
|
||||||
const setDeploymentFormat = (deployments) => {
|
const keyword = (terms?.searchKeyword ?? '').trim();
|
||||||
for (let i = 0; i < deployments.length; i++) {
|
const status = (terms?.searchStatus ?? '').trim();
|
||||||
deployments[i].key = deployments[i].id;
|
return { keyword, status };
|
||||||
}
|
|
||||||
setDeployments(deployments);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status tabs
|
|
||||||
const [activeStatusKey, setActiveStatusKey] = useState('all');
|
|
||||||
const [statusCounts, setStatusCounts] = useState({});
|
|
||||||
|
|
||||||
// Column visibility
|
// Column visibility
|
||||||
const COLUMN_KEYS = useMemo(
|
const COLUMN_KEYS = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -160,114 +156,127 @@ export const useDeploymentsData = () => {
|
|||||||
// Save column visibility to localStorage
|
// Save column visibility to localStorage
|
||||||
const saveColumnVisibility = (newVisibleColumns) => {
|
const saveColumnVisibility = (newVisibleColumns) => {
|
||||||
const normalized = ensureRequiredColumns(newVisibleColumns);
|
const normalized = ensureRequiredColumns(newVisibleColumns);
|
||||||
localStorage.setItem('deployments_visible_columns', JSON.stringify(normalized));
|
localStorage.setItem(
|
||||||
|
'deployments_visible_columns',
|
||||||
|
JSON.stringify(normalized),
|
||||||
|
);
|
||||||
setVisibleColumnsState(normalized);
|
setVisibleColumnsState(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load deployments data
|
const applyDeploymentsData = ({ data, page }) => {
|
||||||
const loadDeployments = async (
|
const items = extractItems(data);
|
||||||
page = 1,
|
setActivePage(data?.page ?? page);
|
||||||
size = pageSize,
|
setDeploymentCount(data?.total ?? items.length);
|
||||||
statusKey = activeStatusKey,
|
setSelectedKeys([]);
|
||||||
) => {
|
setDeployments(
|
||||||
setLoading(true);
|
items.map((deployment) => ({ ...deployment, key: deployment.id })),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDeployments = async ({ page, size, keyword, status }) => {
|
||||||
|
const seq = ++requestSeq.current;
|
||||||
|
const isSearchMode = Boolean(keyword) || Boolean(status);
|
||||||
|
|
||||||
|
if (isSearchMode) {
|
||||||
|
setSearching(true);
|
||||||
|
} else {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url = `/api/deployments/?p=${page}&page_size=${size}`;
|
let url;
|
||||||
if (statusKey && statusKey !== 'all') {
|
if (isSearchMode) {
|
||||||
url = `/api/deployments/search?status=${statusKey}&p=${page}&page_size=${size}`;
|
const params = new URLSearchParams({
|
||||||
|
p: String(page),
|
||||||
|
page_size: String(size),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keyword) params.append('keyword', keyword);
|
||||||
|
if (status) params.append('status', status);
|
||||||
|
|
||||||
|
url = `/api/deployments/search?${params.toString()}`;
|
||||||
|
} else {
|
||||||
|
url = `/api/deployments/?p=${page}&page_size=${size}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await API.get(url);
|
const res = await API.get(url);
|
||||||
const { success, message, data } = res.data;
|
if (seq !== requestSeq.current) return;
|
||||||
if (success) {
|
|
||||||
const newPageData = extractItems(data);
|
|
||||||
setActivePage(data.page || page);
|
|
||||||
setDeploymentCount(data.total || newPageData.length);
|
|
||||||
setDeploymentFormat(newPageData);
|
|
||||||
|
|
||||||
if (data.status_counts) {
|
const { success, message, data } = res.data;
|
||||||
const sumAll = Object.values(data.status_counts).reduce(
|
if (!success) {
|
||||||
(acc, v) => acc + v,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
setStatusCounts({ ...data.status_counts, all: sumAll });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError(message);
|
showError(message);
|
||||||
setDeployments([]);
|
setDeployments([]);
|
||||||
|
setDeploymentCount(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyDeploymentsData({ data, page });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (seq !== requestSeq.current) return;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showError(t('获取部署列表失败'));
|
showError(isSearchMode ? t('搜索失败') : t('获取部署列表失败'));
|
||||||
setDeployments([]);
|
setDeployments([]);
|
||||||
|
setDeploymentCount(0);
|
||||||
|
} finally {
|
||||||
|
if (seq !== requestSeq.current) return;
|
||||||
|
setLoading(false);
|
||||||
|
setSearching(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Search deployments
|
|
||||||
const searchDeployments = async (searchTerms) => {
|
|
||||||
setSearching(true);
|
|
||||||
try {
|
|
||||||
const { searchKeyword, searchStatus } = searchTerms;
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
p: '1',
|
|
||||||
page_size: pageSize.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchKeyword?.trim()) {
|
|
||||||
params.append('keyword', searchKeyword.trim());
|
|
||||||
}
|
|
||||||
if (searchStatus && searchStatus !== 'all') {
|
|
||||||
params.append('status', searchStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await API.get(`/api/deployments/search?${params}`);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
const items = extractItems(data);
|
|
||||||
setActivePage(1);
|
|
||||||
setDeploymentCount(data.total || items.length);
|
|
||||||
setDeploymentFormat(items);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
setDeployments([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search error:', error);
|
|
||||||
showError(t('搜索失败'));
|
|
||||||
setDeployments([]);
|
|
||||||
}
|
|
||||||
setSearching(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
const refresh = async (page = activePage) => {
|
const refresh = async (page = activePage) => {
|
||||||
await loadDeployments(page, pageSize);
|
await fetchDeployments({
|
||||||
|
page,
|
||||||
|
size: pageSize,
|
||||||
|
keyword: query.keyword,
|
||||||
|
status: query.status,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle page change
|
// Handle page change
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
setActivePage(page);
|
setActivePage(page);
|
||||||
if (!searching) {
|
fetchDeployments({
|
||||||
loadDeployments(page, pageSize);
|
page,
|
||||||
}
|
size: pageSize,
|
||||||
|
keyword: query.keyword,
|
||||||
|
status: query.status,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle page size change
|
// Handle page size change
|
||||||
const handlePageSizeChange = (size) => {
|
const handlePageSizeChange = (size) => {
|
||||||
setPageSize(size);
|
setPageSize(size);
|
||||||
setActivePage(1);
|
setActivePage(1);
|
||||||
if (!searching) {
|
fetchDeployments({
|
||||||
loadDeployments(1, size);
|
page: 1,
|
||||||
}
|
size,
|
||||||
|
keyword: query.keyword,
|
||||||
|
status: query.status,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle tab change
|
const loadDeployments = async (page = 1, size = pageSize) => {
|
||||||
const handleTabChange = (statusKey) => {
|
await fetchDeployments({
|
||||||
setActiveStatusKey(statusKey);
|
page,
|
||||||
|
size,
|
||||||
|
keyword: query.keyword,
|
||||||
|
status: query.status,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search deployments (also supports pagination)
|
||||||
|
const searchDeployments = async (searchTerms) => {
|
||||||
|
const nextQuery = normalizeQuery(searchTerms);
|
||||||
|
setQuery(nextQuery);
|
||||||
setActivePage(1);
|
setActivePage(1);
|
||||||
loadDeployments(1, pageSize, statusKey);
|
await fetchDeployments({
|
||||||
|
page: 1,
|
||||||
|
size: pageSize,
|
||||||
|
keyword: nextQuery.keyword,
|
||||||
|
status: nextQuery.status,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deployment operations
|
// Deployment operations
|
||||||
@@ -323,7 +332,9 @@ export const useDeploymentsData = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const containersResp = await API.get(`/api/deployments/${deployment.id}/containers`);
|
const containersResp = await API.get(
|
||||||
|
`/api/deployments/${deployment.id}/containers`,
|
||||||
|
);
|
||||||
if (!containersResp.data?.success) {
|
if (!containersResp.data?.success) {
|
||||||
showError(containersResp.data?.message || t('获取容器信息失败'));
|
showError(containersResp.data?.message || t('获取容器信息失败'));
|
||||||
return;
|
return;
|
||||||
@@ -344,15 +355,20 @@ export const useDeploymentsData = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseName = deployment.container_name || deployment.deployment_name || deployment.name || deployment.id;
|
const baseName =
|
||||||
|
deployment.container_name ||
|
||||||
|
deployment.deployment_name ||
|
||||||
|
deployment.name ||
|
||||||
|
deployment.id;
|
||||||
const safeName = String(baseName || 'ionet').slice(0, 60);
|
const safeName = String(baseName || 'ionet').slice(0, 60);
|
||||||
const channelName = `[IO.NET] ${safeName}`;
|
const channelName = `[IO.NET] ${safeName}`;
|
||||||
|
|
||||||
let randomKey;
|
let randomKey;
|
||||||
try {
|
try {
|
||||||
randomKey = (typeof crypto !== 'undefined' && crypto.randomUUID)
|
randomKey =
|
||||||
? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
|
typeof crypto !== 'undefined' && crypto.randomUUID
|
||||||
: null;
|
? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
|
||||||
|
: null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
randomKey = null;
|
randomKey = null;
|
||||||
}
|
}
|
||||||
@@ -396,7 +412,9 @@ export const useDeploymentsData = () => {
|
|||||||
|
|
||||||
const updateDeploymentName = async (deploymentId, newName) => {
|
const updateDeploymentName = async (deploymentId, newName) => {
|
||||||
try {
|
try {
|
||||||
const res = await API.put(`/api/deployments/${deploymentId}/name`, { name: newName });
|
const res = await API.put(`/api/deployments/${deploymentId}/name`, {
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('部署名称更新成功'));
|
showSuccess(t('部署名称更新成功'));
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -417,7 +435,7 @@ export const useDeploymentsData = () => {
|
|||||||
if (selectedKeys.length === 0) return;
|
if (selectedKeys.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ids = selectedKeys.map(deployment => deployment.id);
|
const ids = selectedKeys.map((deployment) => deployment.id);
|
||||||
const res = await API.post('/api/deployments/batch_delete', { ids });
|
const res = await API.post('/api/deployments/batch_delete', { ids });
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('批量删除成功'));
|
showSuccess(t('批量删除成功'));
|
||||||
@@ -452,8 +470,6 @@ export const useDeploymentsData = () => {
|
|||||||
activePage,
|
activePage,
|
||||||
pageSize,
|
pageSize,
|
||||||
deploymentCount,
|
deploymentCount,
|
||||||
statusCounts,
|
|
||||||
activeStatusKey,
|
|
||||||
compactMode,
|
compactMode,
|
||||||
setCompactMode,
|
setCompactMode,
|
||||||
|
|
||||||
@@ -488,7 +504,6 @@ export const useDeploymentsData = () => {
|
|||||||
refresh,
|
refresh,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handlePageSizeChange,
|
handlePageSizeChange,
|
||||||
handleTabChange,
|
|
||||||
handleRow,
|
handleRow,
|
||||||
|
|
||||||
// Deployment operations
|
// Deployment operations
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
|
|
||||||
// Set loading state for specific operation
|
// Set loading state for specific operation
|
||||||
const setOperationLoading = (operation, deploymentId, isLoading) => {
|
const setOperationLoading = (operation, deploymentId, isLoading) => {
|
||||||
setLoading(prev => ({
|
setLoading((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[`${operation}_${deploymentId}`]: isLoading
|
[`${operation}_${deploymentId}`]: isLoading,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,20 +38,26 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
|
|
||||||
// Extend deployment duration
|
// Extend deployment duration
|
||||||
const extendDeployment = async (deploymentId, durationHours) => {
|
const extendDeployment = async (deploymentId, durationHours) => {
|
||||||
const operationKey = `extend_${deploymentId}`;
|
|
||||||
try {
|
try {
|
||||||
setOperationLoading('extend', deploymentId, true);
|
setOperationLoading('extend', deploymentId, true);
|
||||||
|
|
||||||
const response = await API.post(`/api/deployments/${deploymentId}/extend`, {
|
const response = await API.post(
|
||||||
duration_hours: durationHours
|
`/api/deployments/${deploymentId}/extend`,
|
||||||
});
|
{
|
||||||
|
duration_hours: durationHours,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
showSuccess(t('容器时长延长成功'));
|
showSuccess(t('容器时长延长成功'));
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('延长时长失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('延长时长失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('extend', deploymentId, false);
|
setOperationLoading('extend', deploymentId, false);
|
||||||
@@ -69,7 +75,11 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取详情失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('details', deploymentId, false);
|
setOperationLoading('details', deploymentId, false);
|
||||||
@@ -83,7 +93,8 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (options.containerId) params.append('container_id', options.containerId);
|
if (options.containerId)
|
||||||
|
params.append('container_id', options.containerId);
|
||||||
if (options.level) params.append('level', options.level);
|
if (options.level) params.append('level', options.level);
|
||||||
if (options.limit) params.append('limit', options.limit.toString());
|
if (options.limit) params.append('limit', options.limit.toString());
|
||||||
if (options.cursor) params.append('cursor', options.cursor);
|
if (options.cursor) params.append('cursor', options.cursor);
|
||||||
@@ -91,13 +102,19 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
if (options.startTime) params.append('start_time', options.startTime);
|
if (options.startTime) params.append('start_time', options.startTime);
|
||||||
if (options.endTime) params.append('end_time', options.endTime);
|
if (options.endTime) params.append('end_time', options.endTime);
|
||||||
|
|
||||||
const response = await API.get(`/api/deployments/${deploymentId}/logs?${params}`);
|
const response = await API.get(
|
||||||
|
`/api/deployments/${deploymentId}/logs?${params}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('获取日志失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('logs', deploymentId, false);
|
setOperationLoading('logs', deploymentId, false);
|
||||||
@@ -109,14 +126,21 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
try {
|
try {
|
||||||
setOperationLoading('config', deploymentId, true);
|
setOperationLoading('config', deploymentId, true);
|
||||||
|
|
||||||
const response = await API.put(`/api/deployments/${deploymentId}`, config);
|
const response = await API.put(
|
||||||
|
`/api/deployments/${deploymentId}`,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
showSuccess(t('容器配置更新成功'));
|
showSuccess(t('容器配置更新成功'));
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('更新配置失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('config', deploymentId, false);
|
setOperationLoading('config', deploymentId, false);
|
||||||
@@ -135,7 +159,11 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('销毁容器失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('销毁容器失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('delete', deploymentId, false);
|
setOperationLoading('delete', deploymentId, false);
|
||||||
@@ -148,7 +176,7 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
setOperationLoading('rename', deploymentId, true);
|
setOperationLoading('rename', deploymentId, true);
|
||||||
|
|
||||||
const response = await API.put(`/api/deployments/${deploymentId}/name`, {
|
const response = await API.put(`/api/deployments/${deploymentId}/name`, {
|
||||||
name: newName
|
name: newName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -156,7 +184,11 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('更新名称失败') + ': ' + (error.response?.data?.message || error.message));
|
showError(
|
||||||
|
t('更新名称失败') +
|
||||||
|
': ' +
|
||||||
|
(error.response?.data?.message || error.message),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading('rename', deploymentId, false);
|
setOperationLoading('rename', deploymentId, false);
|
||||||
@@ -169,17 +201,19 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
setOperationLoading('batch_delete', 'all', true);
|
setOperationLoading('batch_delete', 'all', true);
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
deploymentIds.map(id => deleteDeployment(id))
|
deploymentIds.map((id) => deleteDeployment(id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
const successful = results.filter((r) => r.status === 'fulfilled').length;
|
||||||
const failed = results.filter(r => r.status === 'rejected').length;
|
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||||
|
|
||||||
if (successful > 0) {
|
if (successful > 0) {
|
||||||
showSuccess(t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
|
showSuccess(
|
||||||
success: successful,
|
t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
|
||||||
failed: failed
|
success: successful,
|
||||||
}));
|
failed: failed,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { successful, failed };
|
return { successful, failed };
|
||||||
@@ -198,13 +232,16 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
|
|
||||||
const logs = await getDeploymentLogs(deploymentId, {
|
const logs = await getDeploymentLogs(deploymentId, {
|
||||||
...options,
|
...options,
|
||||||
limit: 10000 // Get more logs for export
|
limit: 10000, // Get more logs for export
|
||||||
});
|
});
|
||||||
|
|
||||||
if (logs && logs.logs) {
|
if (logs && logs.logs) {
|
||||||
const logText = logs.logs.map(log =>
|
const logText = logs.logs
|
||||||
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
|
.map(
|
||||||
).join('\n');
|
(log) =>
|
||||||
|
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
const blob = new Blob([logText], { type: 'text/plain' });
|
const blob = new Blob([logText], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -242,7 +279,7 @@ export const useEnhancedDeploymentActions = (t) => {
|
|||||||
loading,
|
loading,
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
setOperationLoading
|
setOperationLoading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { API, toBoolean } from '../../helpers';
|
import { API } from '../../helpers';
|
||||||
|
|
||||||
export const useModelDeploymentSettings = () => {
|
export const useModelDeploymentSettings = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
'model_deployment.ionet.enabled': false,
|
'model_deployment.ionet.enabled': false,
|
||||||
'model_deployment.ionet.api_key': '',
|
|
||||||
});
|
});
|
||||||
const [connectionState, setConnectionState] = useState({
|
const [connectionState, setConnectionState] = useState({
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -35,24 +34,13 @@ export const useModelDeploymentSettings = () => {
|
|||||||
const getSettings = async () => {
|
const getSettings = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/deployments/settings');
|
||||||
const { success, data } = res.data;
|
const { success, data } = res.data;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const newSettings = {
|
setSettings({
|
||||||
'model_deployment.ionet.enabled': false,
|
'model_deployment.ionet.enabled': data?.enabled === true,
|
||||||
'model_deployment.ionet.api_key': '',
|
|
||||||
};
|
|
||||||
|
|
||||||
data.forEach((item) => {
|
|
||||||
if (item.key.endsWith('enabled')) {
|
|
||||||
newSettings[item.key] = toBoolean(item.value);
|
|
||||||
} else if (newSettings.hasOwnProperty(item.key)) {
|
|
||||||
newSettings[item.key] = item.value || '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setSettings(newSettings);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get model deployment settings:', error);
|
console.error('Failed to get model deployment settings:', error);
|
||||||
@@ -65,10 +53,7 @@ export const useModelDeploymentSettings = () => {
|
|||||||
getSettings();
|
getSettings();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const apiKey = settings['model_deployment.ionet.api_key'];
|
const isIoNetEnabled = settings['model_deployment.ionet.enabled'];
|
||||||
const isIoNetEnabled = settings['model_deployment.ionet.enabled'] &&
|
|
||||||
apiKey &&
|
|
||||||
apiKey.trim() !== '';
|
|
||||||
|
|
||||||
const buildConnectionError = (rawMessage, fallbackMessage = 'Connection failed') => {
|
const buildConnectionError = (rawMessage, fallbackMessage = 'Connection failed') => {
|
||||||
const message = (rawMessage || fallbackMessage).trim();
|
const message = (rawMessage || fallbackMessage).trim();
|
||||||
@@ -85,18 +70,12 @@ export const useModelDeploymentSettings = () => {
|
|||||||
return { type: 'unknown', message };
|
return { type: 'unknown', message };
|
||||||
};
|
};
|
||||||
|
|
||||||
const testConnection = useCallback(async (apiKey) => {
|
const testConnection = useCallback(async () => {
|
||||||
const key = (apiKey || '').trim();
|
|
||||||
if (key === '') {
|
|
||||||
setConnectionState({ loading: false, ok: null, error: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConnectionState({ loading: true, ok: null, error: null });
|
setConnectionState({ loading: true, ok: null, error: null });
|
||||||
try {
|
try {
|
||||||
const response = await API.post(
|
const response = await API.post(
|
||||||
'/api/deployments/test-connection',
|
'/api/deployments/settings/test-connection',
|
||||||
{ api_key: key },
|
{},
|
||||||
{ skipErrorHandler: true },
|
{ skipErrorHandler: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,16 +102,15 @@ export const useModelDeploymentSettings = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isIoNetEnabled) {
|
if (!loading && isIoNetEnabled) {
|
||||||
testConnection(apiKey);
|
testConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setConnectionState({ loading: false, ok: null, error: null });
|
setConnectionState({ loading: false, ok: null, error: null });
|
||||||
}, [loading, isIoNetEnabled, apiKey, testConnection]);
|
}, [loading, isIoNetEnabled, testConnection]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
settings,
|
settings,
|
||||||
apiKey,
|
|
||||||
isIoNetEnabled,
|
isIoNetEnabled,
|
||||||
refresh: getSettings,
|
refresh: getSettings,
|
||||||
connectionLoading: connectionState.loading,
|
connectionLoading: connectionState.loading,
|
||||||
|
|||||||
+387
-14
File diff suppressed because it is too large
Load Diff
+387
-52
File diff suppressed because it is too large
Load Diff
+480
-60
File diff suppressed because it is too large
Load Diff
+391
-52
File diff suppressed because it is too large
Load Diff
+1156
-766
File diff suppressed because it is too large
Load Diff
+385
-52
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,6 @@ const ModelDeploymentPage = () => {
|
|||||||
connectionLoading,
|
connectionLoading,
|
||||||
connectionOk,
|
connectionOk,
|
||||||
connectionError,
|
connectionError,
|
||||||
apiKey,
|
|
||||||
testConnection,
|
testConnection,
|
||||||
} = useModelDeploymentSettings();
|
} = useModelDeploymentSettings();
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ const ModelDeploymentPage = () => {
|
|||||||
connectionLoading={connectionLoading}
|
connectionLoading={connectionLoading}
|
||||||
connectionOk={connectionOk}
|
connectionOk={connectionOk}
|
||||||
connectionError={connectionError}
|
connectionError={connectionError}
|
||||||
onRetry={() => testConnection(apiKey)}
|
onRetry={() => testConnection()}
|
||||||
>
|
>
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='mt-[60px] px-2'>
|
||||||
<DeploymentsTable />
|
<DeploymentsTable />
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ export default function SettingModelDeployment(props) {
|
|||||||
|
|
||||||
const testApiKey = async () => {
|
const testApiKey = async () => {
|
||||||
const apiKey = inputs['model_deployment.ionet.api_key'];
|
const apiKey = inputs['model_deployment.ionet.api_key'];
|
||||||
if (!apiKey || apiKey.trim() === '') {
|
|
||||||
showError(t('请先填写 API Key'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLocalizedMessage = (message) => {
|
const getLocalizedMessage = (message) => {
|
||||||
switch (message) {
|
switch (message) {
|
||||||
@@ -69,10 +65,8 @@ export default function SettingModelDeployment(props) {
|
|||||||
setTesting(true);
|
setTesting(true);
|
||||||
try {
|
try {
|
||||||
const response = await API.post(
|
const response = await API.post(
|
||||||
'/api/deployments/test-connection',
|
'/api/deployments/settings/test-connection',
|
||||||
{
|
apiKey && apiKey.trim() !== '' ? { api_key: apiKey.trim() } : {},
|
||||||
api_key: apiKey.trim(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
skipErrorHandler: true,
|
skipErrorHandler: true,
|
||||||
},
|
},
|
||||||
@@ -108,12 +102,6 @@ export default function SettingModelDeployment(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
// 前置校验:如果启用了 io.net 但没有填写 API Key
|
|
||||||
if (inputs['model_deployment.ionet.enabled'] &&
|
|
||||||
(!inputs['model_deployment.ionet.api_key'] || inputs['model_deployment.ionet.api_key'].trim() === '')) {
|
|
||||||
return showError(t('启用 io.net 部署时必须填写 API Key'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateArray = compareObjects(inputs, inputsRow);
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||||
|
|
||||||
@@ -229,7 +217,7 @@ export default function SettingModelDeployment(props) {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label={t('API Key')}
|
label={t('API Key')}
|
||||||
field={'model_deployment.ionet.api_key'}
|
field={'model_deployment.ionet.api_key'}
|
||||||
placeholder={t('请输入 io.net API Key')}
|
placeholder={t('请输入 io.net API Key(敏感信息不显示)')}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
...inputs,
|
...inputs,
|
||||||
@@ -248,9 +236,7 @@ export default function SettingModelDeployment(props) {
|
|||||||
onClick={testApiKey}
|
onClick={testApiKey}
|
||||||
loading={testing}
|
loading={testing}
|
||||||
disabled={
|
disabled={
|
||||||
!inputs['model_deployment.ionet.enabled'] ||
|
!inputs['model_deployment.ionet.enabled']
|
||||||
!inputs['model_deployment.ionet.api_key'] ||
|
|
||||||
inputs['model_deployment.ionet.api_key'].trim() === ''
|
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
height: '32px',
|
height: '32px',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { API, showSuccess, showError } from '../../../helpers';
|
|||||||
import { StatusContext } from '../../../context/Status';
|
import { StatusContext } from '../../../context/Status';
|
||||||
import { UserContext } from '../../../context/User';
|
import { UserContext } from '../../../context/User';
|
||||||
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
|
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
|
||||||
import { useSidebar } from '../../../hooks/common/useSidebar';
|
import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -198,9 +198,25 @@ export default function SettingsSidebarModulesUser() {
|
|||||||
try {
|
try {
|
||||||
// 获取管理员全局配置
|
// 获取管理员全局配置
|
||||||
if (statusState?.status?.SidebarModulesAdmin) {
|
if (statusState?.status?.SidebarModulesAdmin) {
|
||||||
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
|
try {
|
||||||
setAdminConfig(adminConf);
|
const adminConf = JSON.parse(
|
||||||
console.log('加载管理员边栏配置:', adminConf);
|
statusState.status.SidebarModulesAdmin,
|
||||||
|
);
|
||||||
|
const mergedAdminConf = mergeAdminConfig(adminConf);
|
||||||
|
setAdminConfig(mergedAdminConf);
|
||||||
|
console.log('加载管理员边栏配置:', mergedAdminConf);
|
||||||
|
} catch (error) {
|
||||||
|
const mergedAdminConf = mergeAdminConfig(null);
|
||||||
|
setAdminConfig(mergedAdminConf);
|
||||||
|
console.log(
|
||||||
|
'加载管理员边栏配置失败,使用默认配置:',
|
||||||
|
mergedAdminConf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const mergedAdminConf = mergeAdminConfig(null);
|
||||||
|
setAdminConfig(mergedAdminConf);
|
||||||
|
console.log('管理员边栏配置缺失,使用默认配置:', mergedAdminConf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户个人配置
|
// 获取用户个人配置
|
||||||
@@ -323,6 +339,11 @@ export default function SettingsSidebarModulesUser() {
|
|||||||
modules: [
|
modules: [
|
||||||
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
||||||
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
||||||
|
{
|
||||||
|
key: 'deployment',
|
||||||
|
title: t('模型部署'),
|
||||||
|
description: t('模型部署管理'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'redemption',
|
key: 'redemption',
|
||||||
title: t('兑换码管理'),
|
title: t('兑换码管理'),
|
||||||
@@ -389,7 +410,7 @@ export default function SettingsSidebarModulesUser() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={sidebarModulesUser[section.key]?.enabled}
|
checked={sidebarModulesUser[section.key]?.enabled !== false}
|
||||||
onChange={handleSectionChange(section.key)}
|
onChange={handleSectionChange(section.key)}
|
||||||
size='default'
|
size='default'
|
||||||
/>
|
/>
|
||||||
@@ -401,7 +422,9 @@ export default function SettingsSidebarModulesUser() {
|
|||||||
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
|
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
|
||||||
<Card
|
<Card
|
||||||
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
||||||
sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
|
sidebarModulesUser[section.key]?.enabled !== false
|
||||||
|
? ''
|
||||||
|
: 'opacity-50'
|
||||||
}`}
|
}`}
|
||||||
bodyStyle={{ padding: '16px' }}
|
bodyStyle={{ padding: '16px' }}
|
||||||
hoverable
|
hoverable
|
||||||
@@ -417,10 +440,15 @@ export default function SettingsSidebarModulesUser() {
|
|||||||
</div>
|
</div>
|
||||||
<div className='ml-4'>
|
<div className='ml-4'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={sidebarModulesUser[section.key]?.[module.key]}
|
checked={
|
||||||
|
sidebarModulesUser[section.key]?.[module.key] !==
|
||||||
|
false
|
||||||
|
}
|
||||||
onChange={handleModuleChange(section.key, module.key)}
|
onChange={handleModuleChange(section.key, module.key)}
|
||||||
size='default'
|
size='default'
|
||||||
disabled={!sidebarModulesUser[section.key]?.enabled}
|
disabled={
|
||||||
|
sidebarModulesUser[section.key]?.enabled === false
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user