feat(settings): add system status page and improve models page

This commit is contained in:
2025-10-22 16:32:49 +08:00
parent 9137ae3063
commit 73274300ec
7 changed files with 385 additions and 59 deletions

View File

@@ -195,12 +195,12 @@ const Header = () => {
</ListItemIcon>
<ListItemText></ListItemText>
</MenuItem>
{/* 系统设置 */}
<MenuItem onClick={() => navigate('/setting/system')} sx={{ py: 1 }}>
{/* 模型配置 */}
<MenuItem onClick={() => navigate('/setting/models')} sx={{ py: 1 }}>
<ListItemIcon>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText></ListItemText>
<ListItemText></ListItemText>
</MenuItem>
<Divider />

View File

@@ -1,7 +1,7 @@
import { useUserData } from "./useUserData";
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import logger from "@/utils/logger";
import type { IUserInfo } from "@/interfaces/database/user-setting";
import type { IUserInfo, ISystemStatus } from "@/interfaces/database/user-setting";
import userService from "@/services/user_service";
import { rsaPsw } from "../utils/encryption";
import type { IFactory, IMyLlmModel } from "@/interfaces/database/llm";
@@ -92,3 +92,38 @@ export function useLlmModelSetting() {
}
}
/**
* 系统状态设置
*/
export function useSystemStatus() {
const [systemStatus, setSystemStatus] = useState<ISystemStatus | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchSystemStatus = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await userService.system_status();
if (res.data.code === 0) {
setSystemStatus(res.data.data);
} else {
throw new Error(res.data.message || '获取系统状态失败');
}
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.message || '获取系统状态失败';
setError(errorMessage);
logger.error('获取系统状态失败:', error);
} finally {
setLoading(false);
}
}, []);
return {
systemStatus,
loading,
error,
fetchSystemStatus,
};
}

View File

@@ -87,6 +87,8 @@ export interface ISystemStatus {
database: Database;
/** Redis状态 */
redis: Redis;
/** docker引擎状态 */
doc_engine: DockerEngine;
/** 任务执行器心跳信息 */
task_executor_heartbeat: Record<string, TaskExecutorHeartbeatItem[]>;
}
@@ -117,6 +119,8 @@ export interface Storage {
elapsed: number;
/** 错误信息 */
error: string;
/** storage */
storage: string;
}
/**
@@ -130,6 +134,8 @@ export interface Database {
elapsed: number;
/** 错误信息 */
error: string;
/** 数据库名称 */
database: string;
}
/**
@@ -149,6 +155,43 @@ interface Es {
active_shards: number;
}
interface DockerEngine {
/** 活跃主分片数 */
active_primary_shards: number;
/** 活跃分片数 */
active_shards: number;
/** 活跃分片数占比 */
active_shards_percent_as_number: number;
/** 集群名称 */
cluster_name: string;
/** 延迟未分配分片数 */
delayed_unassigned_shards: number;
/** 初始化分片数 */
initializing_shards: number;
/** 数据节点数量 */
number_of_data_nodes: number;
/** 正在处理的获取请求数 */
number_of_in_flight_fetch: number;
/** 节点数量 */
number_of_nodes: number;
/** 待处理任务数 */
number_of_pending_tasks: number;
/** 迁移分片数 */
relocating_shards: number;
/** 服务状态 */
status: string;
/** 任务最大等待时间(毫秒) */
task_max_waiting_in_queue_millis: number;
/** 是否超时 */
timed_out: boolean;
/** 类型 */
type: string;
/** 未分配分片数 */
unassigned_shards: number;
/** 响应耗时 */
elapsed: number;
}
/**
* 租户用户接口
* 定义租户下用户的信息

View File

@@ -47,7 +47,7 @@ export const useDialogState = () => {
};
// API Key 对话框管理
export const useApiKeyDialog = () => {
export const useApiKeyDialog = (onSuccess?: () => void) => {
const dialogState = useDialogState();
const showMessage = useMessage();
const [factoryName, setFactoryName] = useState('');
@@ -77,12 +77,17 @@ export const useApiKeyDialog = () => {
await userService.set_api_key(params);
showMessage.success('API Key 配置成功');
dialogState.closeDialog();
// 调用成功回调
if (onSuccess) {
onSuccess();
}
} catch (error) {
logger.error('API Key 配置失败:', error);
} finally {
dialogState.setLoading(false);
}
}, [factoryName, dialogState]);
}, [factoryName, dialogState, onSuccess]);
return {
...dialogState,
@@ -191,7 +196,7 @@ export const useOllamaDialog = () => {
};
// 删除操作管理
export const useDeleteOperations = () => {
export const useDeleteOperations = (onSuccess?: () => void) => {
const showMessage = useMessage();
const [loading, setLoading] = useState(false);
@@ -203,12 +208,17 @@ export const useDeleteOperations = () => {
llm_name: modelName,
});
showMessage.success('模型删除成功');
// 调用成功回调
if (onSuccess) {
onSuccess();
}
} catch (error) {
logger.error('模型删除失败:', error);
} finally {
setLoading(false);
}
}, []);
}, [onSuccess]);
const deleteFactory = useCallback(async (factoryName: string) => {
setLoading(true);
@@ -217,12 +227,17 @@ export const useDeleteOperations = () => {
llm_factory: factoryName,
});
showMessage.success('模型工厂删除成功');
// 调用成功回调
if (onSuccess) {
onSuccess();
}
} catch (error) {
logger.error('模型工厂删除失败:', error);
} finally {
setLoading(false);
}
}, []);
}, [onSuccess]);
return {
loading,
@@ -232,7 +247,7 @@ export const useDeleteOperations = () => {
};
// 系统默认模型设置
export const useSystemModelSetting = () => {
export const useSystemModelSetting = (onSuccess?: () => void) => {
const dialogState = useDialogState();
const showMessage = useMessage();
@@ -301,6 +316,11 @@ export const useSystemModelSetting = () => {
showMessage.success('系统默认模型设置成功');
dialogState.closeDialog();
fetchTenantInfo();
// 调用成功回调
if (onSuccess) {
onSuccess();
}
} catch (error) {
logger.error('系统默认模型设置失败:', error);
showMessage.error('系统默认模型设置失败');
@@ -308,7 +328,7 @@ export const useSystemModelSetting = () => {
} finally {
dialogState.setLoading(false);
}
}, [dialogState]);
}, [dialogState, onSuccess]);
return {
...dialogState,
@@ -319,13 +339,13 @@ export const useSystemModelSetting = () => {
};
// 统一的模型对话框管理器
export const useModelDialogs = () => {
const apiKeyDialog = useApiKeyDialog();
export const useModelDialogs = (onSuccess?: () => void) => {
const apiKeyDialog = useApiKeyDialog(onSuccess);
const azureDialog = useAzureOpenAIDialog();
const bedrockDialog = useBedrockDialog();
const ollamaDialog = useOllamaDialog();
const systemDialog = useSystemModelSetting();
const deleteOps = useDeleteOperations();
const systemDialog = useSystemModelSetting(onSuccess);
const deleteOps = useDeleteOperations(onSuccess);
// 根据工厂类型打开对应的对话框
const openFactoryDialog = useCallback((factoryName: string, data?: any, isEdit = false) => {

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box,
Typography,
@@ -29,8 +29,6 @@ import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm'
import LLMFactoryCard, { MODEL_TYPE_COLORS } from './components/LLMFactoryCard';
import { ModelDialogs } from './components/ModelDialogs';
import { useDialog } from '@/hooks/useDialog';
import logger from '@/utils/logger';
import { useMessage } from '@/hooks/useSnackbar';
function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model: ILlmItem) => void }) {
return (
@@ -67,12 +65,29 @@ function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model:
// 主页面组件
function ModelsPage() {
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
const modelDialogs = useModelDialogs();
const modelDialogs = useModelDialogs(refreshLlmModel);
// 折叠状态管理 - 使用 Map 来管理每个工厂的折叠状态
const [collapsedFactories, setCollapsedFactories] = useState<Record<string, boolean>>({});
// 切换工厂折叠状态
const toggleFactoryCollapse = useCallback((factoryName: string) => {
setCollapsedFactories(prev => ({
...prev,
[factoryName]: !prev[factoryName]
}));
}, []);
const showLlmFactory = useMemo(() => {
const modelFactoryNames = Object.keys(myLlm || {});
const filterFactory = llmFactory?.filter(factory => !modelFactoryNames.includes(factory.name));
return filterFactory || [];
}, [llmFactory, myLlm]);
// 处理配置模型工厂
const handleConfigureFactory = useCallback((factory: IFactory) => {
modelDialogs.apiKeyDialog.openApiKeyDialog(factory.name);
}, [modelDialogs, refreshLlmModel]);
}, [modelDialogs]);
const dialog = useDialog();
@@ -84,10 +99,9 @@ function ModelsPage() {
showCancel: true,
onConfirm: async () => {
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
await refreshLlmModel();
},
});
}, [dialog, refreshLlmModel]);
}, [dialog, modelDialogs.deleteOps]);
// 处理删除模型工厂
const handleDeleteFactory = useCallback(async (factoryName: string) => {
@@ -97,10 +111,9 @@ function ModelsPage() {
showCancel: true,
onConfirm: async () => {
await modelDialogs.deleteOps.deleteFactory(factoryName);
await refreshLlmModel();
},
});
}, [dialog, refreshLlmModel]);
}, [dialog, modelDialogs.deleteOps]);
if (!llmFactory || !myLlm) {
return (
@@ -150,29 +163,46 @@ function ModelsPage() {
<Grid size={12} key={factoryName}>
<Card variant="outlined">
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
{/* 模型工厂名称 */}
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography>
{/* 模型标签 */}
<Box display="flex" gap={1} mb={2}>
{group.tags.split(',').map((tag) => (
<Chip
key={tag}
label={tag.trim()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
}}
/>
))}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)'
}
}}
onClick={() => toggleFactoryCollapse(factoryName)}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* 折叠/展开图标 */}
<IconButton size="small">
{collapsedFactories[factoryName] ? <ExpandMoreIcon /> : <ExpandLessIcon />}
</IconButton>
<Box>
{/* 模型工厂名称 */}
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography>
{/* 模型标签 */}
<Box display="flex" gap={1} mb={2}>
{group.tags.split(',').map((tag) => (
<Chip
key={tag}
label={tag.trim()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
}}
/>
))}
</Box>
</Box>
</Box>
{/* edit and delete factory button */}
<Box sx={{ display: 'flex', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 1 }} onClick={(e) => e.stopPropagation()}>
<Button
variant='contained' color='primary' startIcon={<EditIcon />}
onClick={() => modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName)}
@@ -187,16 +217,20 @@ function ModelsPage() {
</Button>
</Box>
</Box>
{/* 模型列表 */}
<Grid container spacing={2}>
{group.llm.map((model) => (
<MyLlmGridItem
key={model.name}
model={model}
onDelete={() => handleDeleteModel(factoryName, model.name)}
/>
))}
</Grid>
{/* 模型列表 - 使用 Collapse 组件包装 */}
<Collapse in={!collapsedFactories[factoryName]} timeout="auto" unmountOnExit>
<Box sx={{ mt: 2 }}>
<Grid container spacing={2}>
{group.llm.map((model) => (
<MyLlmGridItem
key={model.name}
model={model}
onDelete={() => handleDeleteModel(factoryName, model.name)}
/>
))}
</Grid>
</Box>
</Collapse>
</CardContent>
</Card>
</Grid>
@@ -218,7 +252,7 @@ function ModelsPage() {
<AccordionDetails>
<Grid container spacing={2}>
{
llmFactory.map((factory) => (
showLlmFactory.map((factory) => (
<Grid size={{ xs: 6, sm: 4, md: 3 }} key={factory.name}>
<LLMFactoryCard
key={factory.name}

View File

@@ -1,8 +1,195 @@
import React, { useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Grid,
Chip,
CircularProgress,
Alert,
Divider,
Paper,
} from '@mui/material';
import {
Storage as StorageIcon,
Memory as RedisIcon,
Search as SearchIcon,
Computer as DocEngineIcon,
Favorite as HeartbeatIcon,
Settings as DefaultIcon,
} from '@mui/icons-material';
import { useSystemStatus } from '@/hooks/setting-hooks';
import type { ISystemStatus } from '@/interfaces/database/user-setting';
// 状态颜色映射
const STATUS_COLORS = {
green: 'success' as const,
red: 'error' as const,
yellow: 'warning' as const,
};
// 组件标题映射
const TITLE_MAP = {
doc_engine: 'Doc Engine',
storage: 'Object Storage',
redis: 'Redis',
database: 'Database',
es: 'Elasticsearch',
task_executor_heartbeat: 'Task Executor',
};
// 图标映射
const ICON_MAP = {
es: SearchIcon,
doc_engine: DocEngineIcon,
redis: RedisIcon,
storage: StorageIcon,
task_executor_heartbeat: HeartbeatIcon,
};
function SystemSetting() {
const { systemStatus, loading, error, fetchSystemStatus } = useSystemStatus();
useEffect(() => {
fetchSystemStatus();
}, [fetchSystemStatus]);
const renderSystemInfo = (key: string, info: any) => {
// 跳过task_executor_heartbeat因为它需要特殊的图表组件
if (key.startsWith('task_executor_heartbeat')) {
return null;
}
const IconComponent = ICON_MAP[key as keyof typeof ICON_MAP] || DefaultIcon;
const title = TITLE_MAP[key as keyof typeof TITLE_MAP] || key;
const status = info?.status || 'unknown';
const chipColor = STATUS_COLORS[status as keyof typeof STATUS_COLORS] || 'default';
return (
<Grid size={12} key={key}>
<Card
sx={{
height: '100%',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 4,
}
}}
>
<CardContent>
<Box display="flex" alignItems="center" gap={2} mb={2}>
<Box
sx={{
p: 1,
borderRadius: 2,
backgroundColor: 'primary.main',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconComponent />
</Box>
<Typography variant="h6" component="h3" sx={{ flexGrow: 1 }}>
{title}
</Typography>
<Chip
label={status.toUpperCase()}
color={chipColor}
size="small"
variant="filled"
sx={{ fontWeight: 'bold' }}
/>
</Box>
<Divider sx={{ mb: 2 }} />
<Box>
{Object.keys(info)
.filter((x) => x !== 'status')
.map((x) => (
<Box
key={x}
display="flex"
justifyContent="space-between"
alignItems="center"
mb={1}
sx={{
p: 1,
borderRadius: 1,
'&:hover': {
backgroundColor: 'grey.50',
}
}}
>
<Typography variant="body2" color="text.secondary" fontWeight="medium">
{x.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
</Typography>
<Typography
variant="body2"
color={x === 'error' ? 'error' : 'text.primary'}
fontWeight="bold"
>
{typeof info[x] === 'number'
? info[x].toFixed(2)
: (info[x] != null ? String(info[x]) : 'N/A')
}
{x === 'elapsed' && ' ms'}
</Typography>
</Box>
))}
</Box>
</CardContent>
</Card>
</Grid>
);
};
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box p={3}>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
</Box>
);
}
return (
<div>
<h1>System Setting</h1>
</div>
<Box p={3}>
<Box mb={4}>
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
</Typography>
</Box>
{systemStatus ? (
<Grid container spacing={3}>
{Object.keys(systemStatus).map((key) =>
renderSystemInfo(key, systemStatus[key as keyof ISystemStatus])
)}
</Grid>
) : (
<Alert severity="info" sx={{ mt: 2 }}>
</Alert>
)}
</Box>
);
}

View File

@@ -114,6 +114,13 @@ const userService = {
set_api_key: (data: ISetApiKeyRequestBody) => {
return request.post(api.set_api_key, data);
},
/* system status */
// 获取系统状态
system_status: () => {
return request.get(api.getSystemStatus);
},
};
export default userService;