Skip to content

Comments

feat: enhance instance details and server management #1169#1338

Closed
zaixizaiximeow wants to merge 1 commit intoUNIkeEN:mainfrom
zaixizaiximeow:fix-server-ping
Closed

feat: enhance instance details and server management #1169#1338
zaixizaiximeow wants to merge 1 commit intoUNIkeEN:mainfrom
zaixizaiximeow:fix-server-ping

Conversation

@zaixizaiximeow
Copy link
Contributor

Checklist

  • Changes have been tested locally and work as expected.
  • All tests in workflows pass successfully.
  • Documentation has been updated if necessary.
  • Code formatting and commit messages align with the project's conventions.
  • Comments have been added for any complex logic or functionality if possible.

This PR is a ..

  • 🆕 New feature
  • 🐞 Bug fix
  • 🛠 Refactoring
  • ⚡️ Performance improvement
  • 🌐 Internationalization
  • 📄 Documentation improvement
  • 🎨 Code style optimization
  • ❓ Other (Please specify below)

Related Issues

closes #1169

Description

新增添加管理服务器功能,且新增输入服务器地址时预览功能(包含防抖)
p2是查询的加载动画
屏幕截图 2026-01-27 165646
506523cc9e80ec28a47efe95d3b0ca0e
image

Copy link
Owner

@UNIkeEN UNIkeEN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此功能与 PR#1328 大部分重复

"versionAsc" => {
summary_list.sort_by(|a, b| {
version_cmp_fn(&a.version, &b.version)
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
Copy link
Owner

@UNIkeEN UNIkeEN Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此处删除了其他 PR 的修复逻辑

use url::Url;
use zip::read::ZipArchive;
// [new]
//use base64::{Engine as _, engine::general_purpose};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请删除 vibe 痕迹

Ok(results) => {
if let Some(res) = results.first() {
Ok(PingResult {
// 映射结果
Copy link
Owner

@UNIkeEN UNIkeEN Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请与项目其他代码统一,使用英文注释

favicon: res.icon_src.clone(),
})
} else {
Err("服务器未响应".to_string())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请使用 Enums 返回错误

const delayTimer = setTimeout(async () => {
setIsQuerying(true);
try {
const result = await invoke("ping_server", { address });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不符合前端 service-handleService 格式规范

name,
address,
});
toast({ title: t("Common.success"), status: "success" });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不符合 service-handleService 前端调用接口规范;且返回不应该是 Common.success,下同

!window.confirm(
t("InstanceWorldsPage.serverList.deleteConfirm", { address })
)
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本项目内应使用 generalConfirmDialog 来进行确认,目前从未使用 window.confirm

handleRetrieveGameServerList(false);
} catch (e) {
toast({
title: t("Common.deleteFailed"),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service 函数处理请使用 service-handleService 规范

return () => clearInterval(intervalId);
}, [instanceId, handleRetrieveGameServerList]);

// UI
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

删除了原来有意义的注释,新增的注释可能没有意义

)}
</Section>

{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds server management functionality to the instance details page, allowing users to add and delete servers from an instance's server list with a real-time server preview feature that uses debouncing.

Changes:

  • Adds UI for adding and deleting servers in the instance worlds page
  • Implements a new modal component for adding servers with live server preview
  • Adds three new Tauri backend commands for server management (add, delete, ping)

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 20 comments.

File Description
src/pages/instances/details/[id]/worlds.tsx Adds handlers for server add/delete operations, integrates new AddServerModal component, and adds delete button to server list UI
src/components/modals/add-server-modal.tsx New modal component for adding servers with debounced live preview functionality
src-tauri/src/instance/commands.rs Implements backend commands for adding/deleting servers to servers.dat file and pinging servers for preview

Comment on lines +1 to +212
import {
Badge,
Box,
Button,
FormControl,
FormLabel,
HStack,
Image,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Skeleton,
Text,
VStack,
} from "@chakra-ui/react";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";

interface AddServerModalProps {
isOpen: boolean;
onClose: () => void;
onAdd: (name: string, address: string) => void;
}

export const AddServerModal = ({
isOpen,
onClose,
onAdd,
}: AddServerModalProps) => {
const [name, setName] = useState("");
const [address, setAddress] = useState("");
const [queryResult, setQueryResult] = useState<any>(null);
const [isQuerying, setIsQuerying] = useState(false);

useEffect(() => {
if (!address || !address.includes(".") || address.length < 3) {
setQueryResult(null);
setIsQuerying(false);
return;
}

const delayTimer = setTimeout(async () => {
setIsQuerying(true);
try {
const result = await invoke("ping_server", { address });
setQueryResult(result);
} catch (err) {
setQueryResult({ error: true });
} finally {
setIsQuerying(false);
}
}, 600);

return () => clearTimeout(delayTimer);
}, [address]);

const handleConfirm = () => {
if (address.trim()) {
onAdd(name || "Minecraft Server", address.trim());
handleClose();
}
};

const handleClose = () => {
setName("");
setAddress("");
setQueryResult(null);
onClose();
};

return (
<Modal isOpen={isOpen} onClose={handleClose} isCentered size="sm">
<ModalOverlay backdropFilter="blur(5px)" />
<ModalContent
bg="rgba(25, 25, 25, 0.9)"
color="white"
borderRadius="xl"
border="1px solid rgba(255,255,255,0.1)"
>
<ModalHeader fontSize="md">添加服务器</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4} align="stretch">
<FormControl>
<FormLabel fontSize="xs" opacity={0.6}>
名称 (可选)
</FormLabel>
<Input
variant="filled"
bg="rgba(0,0,0,0.3)"
_hover={{ bg: "rgba(0,0,0,0.4)" }}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</FormControl>

<FormControl isRequired>
<FormLabel fontSize="xs" opacity={0.6}>
地址 (IP / 域名)
</FormLabel>
<Input
variant="filled"
bg="rgba(0,0,0,0.3)"
_hover={{ bg: "rgba(0,0,0,0.4)" }}
placeholder="play.example.com"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
</FormControl>

{/*overview */}
<Box
minH="70px"
p={3}
borderRadius="md"
bg="whiteAlpha.50"
border="1px solid"
borderColor="whiteAlpha.100"
>
{isQuerying ? (
<HStack spacing={3}>
<Skeleton
startColor="whiteAlpha.200"
endColor="whiteAlpha.300"
borderRadius="sm"
boxSize="40px"
/>

<VStack align="start" spacing={2} flex={1}>
<Skeleton
startColor="whiteAlpha.200"
endColor="whiteAlpha.300"
h="12px"
w="80%"
/>
<Skeleton
startColor="whiteAlpha.200"
endColor="whiteAlpha.300"
h="10px"
w="40%"
/>
</VStack>
</HStack>
) : queryResult?.error ? (
<Text fontSize="xs" color="red.300" textAlign="center" py={4}>
无法解析或连接服务器
</Text>
) : queryResult ? (
<HStack spacing={3}>
<Image
src={queryResult.favicon}
alt="Server Favicon"
fallbackSrc="/images/icons/UnknownWorld.webp"
boxSize="40px"
borderRadius="sm"
/>
<VStack align="start" spacing={0} flex={1}>
<Text
fontSize="xs"
fontWeight="bold"
noOfLines={1}
color="blue.200"
>
{queryResult.motd || "Minecraft Server"}
</Text>
<Text fontSize="2xs" opacity={0.6}>
{queryResult.version} • {queryResult.players}/
{queryResult.maxPlayers}
</Text>
</VStack>
<Badge colorScheme="green" variant="subtle" fontSize="2xs">
在线
</Badge>
</HStack>
) : (
<Text
fontSize="xs"
color="whiteAlpha.400"
textAlign="center"
py={4}
>
输入地址后自动预览
</Text>
)}
</Box>
</VStack>
</ModalBody>

<ModalFooter>
<Button size="sm" variant="ghost" mr={3} onClick={handleClose}>
取消
</Button>
<Button
size="sm"
colorScheme="blue"
onClick={handleConfirm}
isDisabled={!address.trim() || isQuerying}
>
确定添加
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

export default AddServerModal;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component contains hardcoded Chinese text strings instead of using the i18n translation system. All user-facing text should be internationalized using the t() function with locale keys. The following strings need to be moved to locale files: "添加服务器" (line 85), "名称 (可选)" (line 91), "地址 (IP / 域名)" (line 104), "无法解析或连接服务器" (line 151), "Minecraft Server" (line 169), "在线" (line 177), "输入地址后自动预览" (line 187), "取消" (line 196), "确定添加" (line 204).

Copilot uses AI. Check for mistakes.
// confirm
if (
!window.confirm(
t("InstanceWorldsPage.serverList.deleteConfirm", { address })
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation key "InstanceWorldsPage.serverList.deleteConfirm" does not exist in the locale files. This needs to be added to all locale files (en.json, zh-Hans.json, zh-Hant.json, ja.json, fr.json) with appropriate translations.

Suggested change
t("InstanceWorldsPage.serverList.deleteConfirm", { address })
`Are you sure you want to delete server "${address}"?`

Copilot uses AI. Check for mistakes.
Comment on lines 102 to 107
"versionAsc" => {
summary_list.sort_by(|a, b| {
version_cmp_fn(&a.version, &b.version)
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
summary_list.sort_by(|a, b| version_cmp_fn(&a.version, &b.version));
}
"versionDesc" => {
summary_list.sort_by(|a, b| {
version_cmp_fn(&b.version, &a.version)
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
summary_list.sort_by(|a, b| version_cmp_fn(&b.version, &a.version));
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of the secondary sort by name (.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))) changes the existing behavior. When instances have the same version, they will no longer be sorted alphabetically by name. This appears to be an unrelated change to the PR's stated purpose of adding server management functionality. If this is intentional, it should be documented in the PR description or made in a separate commit. If unintentional, the secondary sort should be restored.

Copilot uses AI. Check for mistakes.
}: AddServerModalProps) => {
const [name, setName] = useState("");
const [address, setAddress] = useState("");
const [queryResult, setQueryResult] = useState<any>(null);
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queryResult state is typed as any, which bypasses TypeScript's type safety. You should define a proper interface for the server query result that matches the PingResult structure returned from the backend (with fields: motd, version, players, maxPlayers, favicon) or handle the error case with a discriminated union type.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +45
if (!address || !address.includes(".") || address.length < 3) {
setQueryResult(null);
setIsQuerying(false);
return;
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic !address || !address.includes(".") || address.length < 3 is overly simplistic and may reject valid server addresses or accept invalid ones. For example, it would reject "localhost" (a valid address) but accept "a.b" (too short to be useful). Consider implementing proper validation that:

  1. Accepts both domain names and IP addresses (IPv4/IPv6)
  2. Validates domain format using a regex or URL parser
  3. Optionally validates port numbers if included (e.g., "server.com:25565")
  4. Allows "localhost" as a valid address

Copilot uses AI. Check for mistakes.
};

const handleDeleteServer = async (address: string) => {
const versionPath = (summary as any)?.versionPath;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using (summary as any) to bypass TypeScript's type checking is unsafe and can lead to runtime errors. You should properly type the summary object or access versionPath through a properly typed interface. Consider checking the InstanceSummary type definition to see if versionPath should be added as a field, or use a type guard to safely access this property.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +114
const handleDeleteServer = async (address: string) => {
const versionPath = (summary as any)?.versionPath;
if (!versionPath) return;

// confirm
if (
!window.confirm(
t("InstanceWorldsPage.serverList.deleteConfirm", { address })
)
)
return;

try {
await invoke("delete_server_from_instance", {
instancePath: versionPath,
address: address,
});
toast({ title: t("Common.deleteSuccess"), status: "success" });
handleRetrieveGameServerList(false);
} catch (e) {
toast({
title: t("Common.deleteFailed"),
description: String(e),
status: "error",
});
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using window.confirm() is inconsistent with the UI patterns in the rest of the codebase. The project has a GenericConfirmDialog component (see src/components/modals/generic-confirm-dialog.tsx) that provides a styled, consistent confirmation dialog. Consider using this component instead for a better user experience and consistency with the application's design system.

Suggested change
const handleDeleteServer = async (address: string) => {
const versionPath = (summary as any)?.versionPath;
if (!versionPath) return;
// confirm
if (
!window.confirm(
t("InstanceWorldsPage.serverList.deleteConfirm", { address })
)
)
return;
try {
await invoke("delete_server_from_instance", {
instancePath: versionPath,
address: address,
});
toast({ title: t("Common.deleteSuccess"), status: "success" });
handleRetrieveGameServerList(false);
} catch (e) {
toast({
title: t("Common.deleteFailed"),
description: String(e),
status: "error",
});
}
const handleDeleteServer = (address: string) => {
const versionPath = (summary as any)?.versionPath;
if (!versionPath) return;
openSharedModal("confirm", {
title: t("Common.confirm"),
description: t("InstanceWorldsPage.serverList.deleteConfirm", { address }),
confirmText: t("Common.delete"),
confirmColorScheme: "red",
onConfirm: async () => {
try {
await invoke("delete_server_from_instance", {
instancePath: versionPath,
address,
});
toast({ title: t("Common.deleteSuccess"), status: "success" });
handleRetrieveGameServerList(false);
} catch (e) {
toast({
title: t("Common.deleteFailed"),
description: String(e),
status: "error",
});
}
},
});

Copilot uses AI. Check for mistakes.
{/* add server */}
<CommonIconButton
icon={LuPlus}
label="添加服务器"
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This label is hardcoded as Chinese text "添加服务器" instead of using the i18n translation system. It should be replaced with t("InstanceWorldsPage.serverList.addServer") or a similar locale key after adding it to the locale files.

Suggested change
label="添加服务器"
label={t("InstanceWorldsPage.serverList.addServer")}

Copilot uses AI. Check for mistakes.
Comment on lines +1460 to +1463
Err("服务器未响应".to_string())
}
}
Err(e) => Err(format!("查询失败: {:?}", e)),
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These error messages are hardcoded in Chinese ("服务器未响应" and "查询失败"). For consistency with the rest of the codebase, error messages should be in English since they will be displayed in the frontend's error handling which can then apply proper i18n. Change these to English messages like "Server did not respond" and "Query failed".

Suggested change
Err("服务器未响应".to_string())
}
}
Err(e) => Err(format!("查询失败: {:?}", e)),
Err("Server did not respond".to_string())
}
}
Err(e) => Err(format!("Query failed: {:?}", e)),

Copilot uses AI. Check for mistakes.
Comment on lines +1434 to +1465
#[tauri::command]
pub async fn ping_server(address: String) -> Result<PingResult, String> {
let server_infos = vec![GameServerInfo {
name: "Preview".to_string(),
ip: address.clone(),
icon_src: String::new(),
hidden: false,
description: String::new(),
is_queried: false,
online: false,
players_max: 0,
players_online: 0,
}];

match query_servers_online(server_infos).await {
Ok(results) => {
if let Some(res) = results.first() {
Ok(PingResult {
// 映射结果
motd: res.description.clone(),
version: "Unknown".to_string(),
players: res.players_online as u32,
max_players: res.players_max as u32,
favicon: res.icon_src.clone(),
})
} else {
Err("服务器未响应".to_string())
}
}
Err(e) => Err(format!("查询失败: {:?}", e)),
}
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Tauri command ping_server is not registered in the invoke_handler in src-tauri/src/lib.rs. This command will not be accessible from the frontend and will cause runtime errors when invoked. You need to add instance::commands::ping_server to the tauri::generate_handler![] macro in lib.rs.

Copilot uses AI. Check for mistakes.
@UNIkeEN
Copy link
Owner

UNIkeEN commented Feb 6, 2026

基于 #1328 修改并合并此功能,先关闭此 PR~

@UNIkeEN UNIkeEN closed this Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 支持实例服务器列表的编辑

2 participants