feat(panel): align my servers page search layout with fleet table
This commit is contained in:
3 files changed
+72
-64
No files matched your search
@@ -5,6 +5,8 @@
|
|||||||
"filter_status_all": "All statuses",
|
"filter_status_all": "All statuses",
|
||||||
"search_no_match": "No matching servers found.",
|
"search_no_match": "No matching servers found.",
|
||||||
"search_clear_btn": "Clear filters",
|
"search_clear_btn": "Clear filters",
|
||||||
|
"servers_count": "{{count}} servers",
|
||||||
|
"servers_count_filtered": "Showing {{shown}} / {{total}} servers",
|
||||||
"no_servers_linked": "No servers linked to you",
|
"no_servers_linked": "No servers linked to you",
|
||||||
"no_servers_hint": "Claim an unowned server you have access to, or ask an admin to provision one.",
|
"no_servers_hint": "Claim an unowned server you have access to, or ask an admin to provision one.",
|
||||||
"my_servers_footer": "Servers and game types are provisioned and managed by the platform. You claim a node to operate it; raw cluster config is never exposed here.",
|
"my_servers_footer": "Servers and game types are provisioned and managed by the platform. You claim a node to operate it; raw cluster config is never exposed here.",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"filter_status_all": "全部状态",
|
"filter_status_all": "全部状态",
|
||||||
"search_no_match": "没有匹配的服务器。",
|
"search_no_match": "没有匹配的服务器。",
|
||||||
"search_clear_btn": "清除筛选",
|
"search_clear_btn": "清除筛选",
|
||||||
|
"servers_count": "共 {{count}} 台",
|
||||||
|
"servers_count_filtered": "显示 {{shown}} / {{total}} 台",
|
||||||
"no_servers_linked": "暂无关联的服务器",
|
"no_servers_linked": "暂无关联的服务器",
|
||||||
"no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。",
|
"no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。",
|
||||||
"my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。",
|
"my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。",
|
||||||
|
|||||||
@@ -45,71 +45,75 @@ export function MyServers() {
|
|||||||
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<Server className="h-6 w-6 text-primary" />
|
||||||
<Server className="h-6 w-6 text-primary" />
|
<div>
|
||||||
<div>
|
<h1 className="text-2xl font-semibold tracking-tight">{t("my_servers_title")}</h1>
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{t("my_servers_title")}</h1>
|
<p className="text-sm text-muted-foreground">
|
||||||
<p className="text-sm text-muted-foreground">
|
{t("my_servers_subtitle")}
|
||||||
{t("my_servers_subtitle")}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{servers.length > 0 && cfg && (
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center w-full md:w-auto">
|
|
||||||
<div className="relative w-full sm:w-64">
|
|
||||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder={t("search_placeholder")}
|
|
||||||
className="h-8 pl-8 pr-8 text-sm"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchQuery(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="w-full sm:w-36">
|
|
||||||
<Select
|
|
||||||
value={statusFilter}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
setStatusFilter(val);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-sm">
|
|
||||||
<SelectValue placeholder={t("filter_status_all")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">{t("filter_status_all")}</SelectItem>
|
|
||||||
<SelectItem value="Running">{t("phase_running")}</SelectItem>
|
|
||||||
<SelectItem value="Starting">{t("phase_starting")}</SelectItem>
|
|
||||||
<SelectItem value="Stopping">{t("phase_stopping")}</SelectItem>
|
|
||||||
<SelectItem value="Stopped">{t("phase_stopped")}</SelectItem>
|
|
||||||
<SelectItem value="Failed">{t("phase_failed")}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{servers.length > 0 && cfg && (
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<div className="relative min-w-[14rem] flex-1">
|
||||||
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={t("search_placeholder")}
|
||||||
|
className="pl-9 pr-9"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setStatusFilter(val);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-44">
|
||||||
|
<SelectValue placeholder={t("filter_status_all")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("filter_status_all")}</SelectItem>
|
||||||
|
<SelectItem value="Running">{t("phase_running")}</SelectItem>
|
||||||
|
<SelectItem value="Starting">{t("phase_starting")}</SelectItem>
|
||||||
|
<SelectItem value="Stopping">{t("phase_stopping")}</SelectItem>
|
||||||
|
<SelectItem value="Stopped">{t("phase_stopped")}</SelectItem>
|
||||||
|
<SelectItem value="Failed">{t("phase_failed")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{filtered.length === servers.length
|
||||||
|
? t("servers_count", { count: servers.length })
|
||||||
|
: t("servers_count_filtered", {
|
||||||
|
shown: filtered.length,
|
||||||
|
total: servers.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && !data ? (
|
{loading && !data ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -151,7 +155,7 @@ export function MyServers() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{filtered.length > 0 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
page={page}
|
page={page}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
@@ -159,6 +163,6 @@ export function MyServers() {
|
|||||||
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user