From 73274300ec2a7b6101e71955f381fc2733ec73b8 Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Wed, 22 Oct 2025 16:32:49 +0800 Subject: [PATCH] feat(settings): add system status page and improve models page --- src/components/Layout/Header.tsx | 6 +- src/hooks/setting-hooks.ts | 39 ++++- src/interfaces/database/user-setting.ts | 43 +++++ src/pages/setting/hooks/useModelDialogs.ts | 42 +++-- src/pages/setting/models.tsx | 114 +++++++----- src/pages/setting/system.tsx | 193 ++++++++++++++++++++- src/services/user_service.ts | 7 + 7 files changed, 385 insertions(+), 59 deletions(-) diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 73575f1..8846fd6 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -195,12 +195,12 @@ const Header = () => { 个人资料 - {/* 系统设置 */} - navigate('/setting/system')} sx={{ py: 1 }}> + {/* 模型配置 */} + navigate('/setting/models')} sx={{ py: 1 }}> - 系统设置 + 模型配置 diff --git a/src/hooks/setting-hooks.ts b/src/hooks/setting-hooks.ts index 1f6425a..f2b074a 100644 --- a/src/hooks/setting-hooks.ts +++ b/src/hooks/setting-hooks.ts @@ -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(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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, + }; +} + diff --git a/src/interfaces/database/user-setting.ts b/src/interfaces/database/user-setting.ts index a4c65d7..730a974 100644 --- a/src/interfaces/database/user-setting.ts +++ b/src/interfaces/database/user-setting.ts @@ -87,6 +87,8 @@ export interface ISystemStatus { database: Database; /** Redis状态 */ redis: Redis; + /** docker引擎状态 */ + doc_engine: DockerEngine; /** 任务执行器心跳信息 */ task_executor_heartbeat: Record; } @@ -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; +} + /** * 租户用户接口 * 定义租户下用户的信息 diff --git a/src/pages/setting/hooks/useModelDialogs.ts b/src/pages/setting/hooks/useModelDialogs.ts index be5aa53..ef0b713 100644 --- a/src/pages/setting/hooks/useModelDialogs.ts +++ b/src/pages/setting/hooks/useModelDialogs.ts @@ -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) => { diff --git a/src/pages/setting/models.tsx b/src/pages/setting/models.tsx index 337dfd2..9dbba7f 100644 --- a/src/pages/setting/models.tsx +++ b/src/pages/setting/models.tsx @@ -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>({}); + + // 切换工厂折叠状态 + 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() { - - - {/* 模型工厂名称 */} - - {factoryName} - - {/* 模型标签 */} - - {group.tags.split(',').map((tag) => ( - - ))} + toggleFactoryCollapse(factoryName)} + > + + {/* 折叠/展开图标 */} + + {collapsedFactories[factoryName] ? : } + + + {/* 模型工厂名称 */} + + {factoryName} + + {/* 模型标签 */} + + {group.tags.split(',').map((tag) => ( + + ))} + {/* edit and delete factory button */} - + e.stopPropagation()}> - {/* 模型列表 */} - - {group.llm.map((model) => ( - handleDeleteModel(factoryName, model.name)} - /> - ))} - + {/* 模型列表 - 使用 Collapse 组件包装 */} + + + + {group.llm.map((model) => ( + handleDeleteModel(factoryName, model.name)} + /> + ))} + + + @@ -218,7 +252,7 @@ function ModelsPage() { { - llmFactory.map((factory) => ( + showLlmFactory.map((factory) => ( { + 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 ( + + + + + + + + + {title} + + + + + + + + {Object.keys(info) + .filter((x) => x !== 'status') + .map((x) => ( + + + {x.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: + + + {typeof info[x] === 'number' + ? info[x].toFixed(2) + : (info[x] != null ? String(info[x]) : 'N/A') + } + {x === 'elapsed' && ' ms'} + + + ))} + + + + + ); + }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + ); + } + return ( -
-

System Setting

-
+ + + + 系统状态 + + + + 查看系统各个组件的运行状态和性能指标 + + + + {systemStatus ? ( + + {Object.keys(systemStatus).map((key) => + renderSystemInfo(key, systemStatus[key as keyof ISystemStatus]) + )} + + ) : ( + + 暂无系统状态数据 + + )} + ); } diff --git a/src/services/user_service.ts b/src/services/user_service.ts index 2ce7519..57b37f9 100644 --- a/src/services/user_service.ts +++ b/src/services/user_service.ts @@ -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; \ No newline at end of file