feat: improve async operations and UI feedback

This commit is contained in:
Kadxy 2025-01-16 21:30:15 +08:00
parent 4d535b1cd0
commit 65810d918b
3 changed files with 201 additions and 80 deletions

View File

@ -85,6 +85,50 @@
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
} }
&.loading {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
background-size: 200% 100%;
animation: loading-pulse 1.5s infinite;
}
}
.operation-status {
display: inline-flex;
align-items: center;
margin-left: 10px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background-color: #16a34a;
color: #fff;
animation: pulse 1.5s infinite;
&[data-status="stopping"] {
background-color: #9ca3af;
}
&[data-status="starting"] {
background-color: #4ade80;
}
&[data-status="error"] {
background-color: #f87171;
}
}
.mcp-market-header { .mcp-market-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -585,3 +629,24 @@
} }
} }
} }
@keyframes loading-pulse {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}

View File

@ -52,6 +52,9 @@ export function McpMarketPage() {
>({}); >({});
const [loadingPresets, setLoadingPresets] = useState(true); const [loadingPresets, setLoadingPresets] = useState(true);
const [presetServers, setPresetServers] = useState<PresetServer[]>([]); const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
{},
);
useEffect(() => { useEffect(() => {
const loadPresetServers = async () => { const loadPresetServers = async () => {
@ -141,8 +144,12 @@ export function McpMarketPage() {
const preset = presetServers.find((s) => s.id === editingServerId); const preset = presetServers.find((s) => s.id === editingServerId);
if (!preset || !preset.configSchema || !editingServerId) return; if (!preset || !preset.configSchema || !editingServerId) return;
// 先关闭模态框
const savingServerId = editingServerId;
setEditingServerId(undefined);
try { try {
setIsLoading(true); updateLoadingState(savingServerId, "Updating configuration...");
// 构建服务器配置 // 构建服务器配置
const args = [...preset.baseArgs]; const args = [...preset.baseArgs];
const env: Record<string, string> = {}; const env: Record<string, string> = {};
@ -172,25 +179,38 @@ export function McpMarketPage() {
...(Object.keys(env).length > 0 ? { env } : {}), ...(Object.keys(env).length > 0 ? { env } : {}),
}; };
// 检查是否是新增还是编辑
const isNewServer = !isServerAdded(savingServerId);
// 如果是编辑现有服务器,保持原有状态
if (!isNewServer) {
const currentConfig = await getMcpConfigFromFile();
const currentStatus = currentConfig.mcpServers[savingServerId]?.status;
if (currentStatus) {
serverConfig.status = currentStatus;
}
}
// 更新配置并初始化新服务器 // 更新配置并初始化新服务器
const newConfig = await addMcpServer(editingServerId, serverConfig); const newConfig = await addMcpServer(savingServerId, serverConfig);
setConfig(newConfig); setConfig(newConfig);
// 更新状态 // 只有新增的服务器才需要获取状态(因为会自动启动)
const status = await getClientStatus(editingServerId); if (isNewServer) {
const status = await getClientStatus(savingServerId);
setClientStatuses((prev) => ({ setClientStatuses((prev) => ({
...prev, ...prev,
[editingServerId]: status, [savingServerId]: status,
})); }));
}
setEditingServerId(undefined); showToast("Server configuration updated successfully");
showToast("Server configuration saved successfully");
} catch (error) { } catch (error) {
showToast( showToast(
error instanceof Error ? error.message : "Failed to save configuration", error instanceof Error ? error.message : "Failed to save configuration",
); );
} finally { } finally {
setIsLoading(false); updateLoadingState(savingServerId, null);
} }
}; };
@ -210,36 +230,24 @@ export function McpMarketPage() {
} }
}; };
// 重启所有客户端 // 更新加载状态的辅助函数
const handleRestartAll = async () => { const updateLoadingState = (id: string, message: string | null) => {
try { setLoadingStates((prev) => {
setIsLoading(true); if (message === null) {
const newConfig = await restartAllClients(); const { [id]: _, ...rest } = prev;
setConfig(newConfig); return rest;
// 更新所有客户端状态
const statuses: Record<string, any> = {};
for (const clientId of Object.keys(newConfig.mcpServers)) {
statuses[clientId] = await getClientStatus(clientId);
}
setClientStatuses(statuses);
showToast("Successfully restarted all clients");
} catch (error) {
showToast("Failed to restart clients");
console.error(error);
} finally {
setIsLoading(false);
} }
return { ...prev, [id]: message };
});
}; };
// 添加服务器 // 修改添加服务器函数
const addServer = async (preset: PresetServer) => { const addServer = async (preset: PresetServer) => {
if (!preset.configurable) { if (!preset.configurable) {
try { try {
setIsLoading(true); const serverId = preset.id;
showToast("Creating MCP client..."); updateLoadingState(serverId, "Creating MCP client...");
// 如果服务器不需要配置,直接添加
const serverConfig: ServerConfig = { const serverConfig: ServerConfig = {
command: preset.command, command: preset.command,
args: [...preset.baseArgs], args: [...preset.baseArgs],
@ -254,7 +262,7 @@ export function McpMarketPage() {
[preset.id]: status, [preset.id]: status,
})); }));
} finally { } finally {
setIsLoading(false); updateLoadingState(preset.id, null);
} }
} else { } else {
// 如果需要配置,打开配置对话框 // 如果需要配置,打开配置对话框
@ -263,33 +271,13 @@ export function McpMarketPage() {
} }
}; };
// 移除服务器 // 修改暂停服务器函数
// const removeServer = async (id: string) => {
// try {
// setIsLoading(true);
// const newConfig = await removeMcpServer(id);
// setConfig(newConfig);
// // 移除状态
// setClientStatuses((prev) => {
// const newStatuses = { ...prev };
// delete newStatuses[id];
// return newStatuses;
// });
// } finally {
// setIsLoading(false);
// }
// };
// 暂停服务器
const pauseServer = async (id: string) => { const pauseServer = async (id: string) => {
try { try {
setIsLoading(true); updateLoadingState(id, "Stopping server...");
showToast("Stopping server...");
const newConfig = await pauseMcpServer(id); const newConfig = await pauseMcpServer(id);
setConfig(newConfig); setConfig(newConfig);
// 更新状态为暂停
setClientStatuses((prev) => ({ setClientStatuses((prev) => ({
...prev, ...prev,
[id]: { status: "paused", errorMsg: null }, [id]: { status: "paused", errorMsg: null },
@ -299,27 +287,22 @@ export function McpMarketPage() {
showToast("Failed to stop server"); showToast("Failed to stop server");
console.error(error); console.error(error);
} finally { } finally {
setIsLoading(false); updateLoadingState(id, null);
} }
}; };
// 恢复服务器 // 修改恢复服务器函数
const resumeServer = async (id: string) => { const resumeServer = async (id: string) => {
try { try {
setIsLoading(true); updateLoadingState(id, "Starting server...");
showToast("Starting server...");
// 尝试启动服务器
const success = await resumeMcpServer(id); const success = await resumeMcpServer(id);
// 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态)
const status = await getClientStatus(id); const status = await getClientStatus(id);
setClientStatuses((prev) => ({ setClientStatuses((prev) => ({
...prev, ...prev,
[id]: status, [id]: status,
})); }));
// 根据启动结果显示消息
if (success) { if (success) {
showToast("Server started successfully"); showToast("Server started successfully");
} else { } else {
@ -333,7 +316,29 @@ export function McpMarketPage() {
); );
console.error(error); console.error(error);
} finally { } finally {
setIsLoading(false); updateLoadingState(id, null);
}
};
// 修改重启所有客户端函数
const handleRestartAll = async () => {
try {
updateLoadingState("all", "Restarting all servers...");
const newConfig = await restartAllClients();
setConfig(newConfig);
const statuses: Record<string, any> = {};
for (const clientId of Object.keys(newConfig.mcpServers)) {
statuses[clientId] = await getClientStatus(clientId);
}
setClientStatuses(statuses);
showToast("Successfully restarted all clients");
} catch (error) {
showToast("Failed to restart clients");
console.error(error);
} finally {
updateLoadingState("all", null);
} }
}; };
@ -445,6 +450,14 @@ export function McpMarketPage() {
return statusMap[status.status]; return statusMap[status.status];
}; };
// 获取操作状态的类型
const getOperationStatusType = (message: string) => {
if (message.toLowerCase().includes("stopping")) return "stopping";
if (message.toLowerCase().includes("starting")) return "starting";
if (message.toLowerCase().includes("error")) return "error";
return "default";
};
// 渲染服务器列表 // 渲染服务器列表
const renderServerList = () => { const renderServerList = () => {
if (loadingPresets) { if (loadingPresets) {
@ -478,29 +491,46 @@ export function McpMarketPage() {
.sort((a, b) => { .sort((a, b) => {
const aStatus = checkServerStatus(a.id).status; const aStatus = checkServerStatus(a.id).status;
const bStatus = checkServerStatus(b.id).status; const bStatus = checkServerStatus(b.id).status;
const aLoading = loadingStates[a.id];
const bLoading = loadingStates[b.id];
// 定义状态优先级 // 定义状态优先级
const statusPriority: Record<string, number> = { const statusPriority: Record<string, number> = {
error: 0, // 最高优先级 error: 0, // 错误状态最高优先级
active: 1, // 运行中 active: 1, // 已启动次之
paused: 2, // 已暂停 starting: 2, // 正在启动
undefined: 3, // 未配置/未找到 stopping: 3, // 正在停止
paused: 4, // 已暂停
undefined: 5, // 未配置最低优先级
}; };
// 获取实际状态(包括加载状态)
const getEffectiveStatus = (status: string, loading?: string) => {
if (loading) {
const operationType = getOperationStatusType(loading);
return operationType === "default" ? status : operationType;
}
return status;
};
const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
// 首先按状态排序 // 首先按状态排序
if (aStatus !== bStatus) { if (aEffectiveStatus !== bEffectiveStatus) {
return ( return (
(statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3) (statusPriority[aEffectiveStatus] ?? 5) -
(statusPriority[bEffectiveStatus] ?? 5)
); );
} }
// 然后按名称排序 // 状态相同时按名称排序
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}) })
.map((server) => ( .map((server) => (
<div <div
className={clsx(styles["mcp-market-item"], { className={clsx(styles["mcp-market-item"], {
[styles["disabled"]]: isLoading, [styles["loading"]]: loadingStates[server.id],
})} })}
key={server.id} key={server.id}
> >
@ -508,7 +538,17 @@ export function McpMarketPage() {
<div className={styles["mcp-market-title"]}> <div className={styles["mcp-market-title"]}>
<div className={styles["mcp-market-name"]}> <div className={styles["mcp-market-name"]}>
{server.name} {server.name}
{getServerStatusDisplay(server.id)} {loadingStates[server.id] && (
<span
className={styles["operation-status"]}
data-status={getOperationStatusType(
loadingStates[server.id],
)}
>
{loadingStates[server.id]}
</span>
)}
{!loadingStates[server.id] && getServerStatusDisplay(server.id)}
{server.repo && ( {server.repo && (
<a <a
href={server.repo} href={server.repo}
@ -605,8 +645,10 @@ export function McpMarketPage() {
<div className="window-header-title"> <div className="window-header-title">
<div className="window-header-main-title"> <div className="window-header-main-title">
MCP Market MCP Market
{isLoading && ( {loadingStates["all"] && (
<span className={styles["loading-indicator"]}>Loading...</span> <span className={styles["loading-indicator"]}>
{loadingStates["all"]}
</span>
)} )}
</div> </div>
<div className="window-header-sub-title"> <div className="window-header-sub-title">

View File

@ -98,6 +98,9 @@ async function initializeSingleClient(
try { try {
const client = await createClient(clientId, serverConfig); const client = await createClient(clientId, serverConfig);
const tools = await listTools(client); const tools = await listTools(client);
logger.info(
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
);
clientsMap.set(clientId, { client, tools, errorMsg: null }); clientsMap.set(clientId, { client, tools, errorMsg: null });
logger.success(`Client [${clientId}] initialized successfully`); logger.success(`Client [${clientId}] initialized successfully`);
} catch (error) { } catch (error) {
@ -130,6 +133,13 @@ export async function initializeMcpSystem() {
export async function addMcpServer(clientId: string, config: ServerConfig) { export async function addMcpServer(clientId: string, config: ServerConfig) {
try { try {
const currentConfig = await getMcpConfigFromFile(); const currentConfig = await getMcpConfigFromFile();
const isNewServer = !(clientId in currentConfig.mcpServers);
// 如果是新服务器,设置默认状态为 active
if (isNewServer && !config.status) {
config.status = "active";
}
const newConfig = { const newConfig = {
...currentConfig, ...currentConfig,
mcpServers: { mcpServers: {
@ -138,8 +148,12 @@ export async function addMcpServer(clientId: string, config: ServerConfig) {
}, },
}; };
await updateMcpConfig(newConfig); await updateMcpConfig(newConfig);
// 只初始化新添加的服务器
// 只有新服务器或状态为 active 的服务器才初始化
if (isNewServer || config.status === "active") {
await initializeSingleClient(clientId, config); await initializeSingleClient(clientId, config);
}
return newConfig; return newConfig;
} catch (error) { } catch (error) {
logger.error(`Failed to add server [${clientId}]: ${error}`); logger.error(`Failed to add server [${clientId}]: ${error}`);