feat(panel): add search and status filtering to my servers page
This commit is contained in:
3 files changed
+104
-5
No files matched your search
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"my_servers_title": "My servers",
|
"my_servers_title": "My servers",
|
||||||
"my_servers_subtitle": "Wake, stop, or claim the servers you can manage.",
|
"my_servers_subtitle": "Wake, stop, or claim the servers you can manage.",
|
||||||
|
"search_placeholder": "Search server name or address…",
|
||||||
|
"filter_status_all": "All statuses",
|
||||||
|
"search_no_match": "No matching servers found.",
|
||||||
|
"search_clear_btn": "Clear filters",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"my_servers_title": "我的服务器",
|
"my_servers_title": "我的服务器",
|
||||||
"my_servers_subtitle": "管理你拥有或可用的服务器。",
|
"my_servers_subtitle": "管理你拥有或可用的服务器。",
|
||||||
|
"search_placeholder": "搜索服务器名称或地址…",
|
||||||
|
"filter_status_all": "全部状态",
|
||||||
|
"search_no_match": "没有匹配的服务器。",
|
||||||
|
"search_clear_btn": "清除筛选",
|
||||||
"no_servers_linked": "暂无关联的服务器",
|
"no_servers_linked": "暂无关联的服务器",
|
||||||
"no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。",
|
"no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。",
|
||||||
"my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。",
|
"my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。",
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Server } from "lucide-react";
|
import { Server, Search, X } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { ServerCard } from "@/components/ServerCard";
|
import { ServerCard } from "@/components/ServerCard";
|
||||||
import { Pagination } from "@/components/Pagination";
|
import { Pagination } from "@/components/Pagination";
|
||||||
import { Loading, ErrorState, EmptyState } from "@/components/States";
|
import { Loading, ErrorState, EmptyState } from "@/components/States";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useAsync, useConfig } from "@/lib/hooks";
|
import { useAsync, useConfig } from "@/lib/hooks";
|
||||||
|
import { hostFor } from "@/lib/config";
|
||||||
|
|
||||||
const PAGE_SIZE = 12;
|
const PAGE_SIZE = 12;
|
||||||
|
|
||||||
@@ -16,8 +20,29 @@ export function MyServers() {
|
|||||||
const { t } = useTranslation("servers");
|
const { t } = useTranslation("servers");
|
||||||
const servers = data ?? [];
|
const servers = data ?? [];
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const totalPages = Math.max(1, Math.ceil(servers.length / PAGE_SIZE));
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const paged = servers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
|
||||||
|
const filtered = servers.filter((s) => {
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
const q = searchQuery.toLowerCase().trim();
|
||||||
|
const host = cfg ? hostFor(s.subdomain, cfg).toLowerCase() : "";
|
||||||
|
const name = s.name.toLowerCase();
|
||||||
|
const displayName = (s.displayName || "").toLowerCase();
|
||||||
|
if (!name.includes(q) && !displayName.includes(q) && !host.includes(q)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusFilter !== "all") {
|
||||||
|
if (s.phase !== statusFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||||
|
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -31,6 +56,56 @@ export function MyServers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{servers.length > 0 && cfg && (
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center my-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={t("search_placeholder")}
|
||||||
|
className="pl-9 pr-8"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="absolute right-2.5 top-2.5 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-[180px]">
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setStatusFilter(val);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && !data ? (
|
{loading && !data ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -42,6 +117,22 @@ export function MyServers() {
|
|||||||
title={t("no_servers_linked")}
|
title={t("no_servers_linked")}
|
||||||
hint={t("no_servers_hint")}
|
hint={t("no_servers_hint")}
|
||||||
/>
|
/>
|
||||||
|
) : filtered.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center border border-dashed rounded-lg border-border bg-card/50">
|
||||||
|
<p className="text-sm font-medium text-foreground">{t("search_no_match")}</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setStatusFilter("all");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("search_clear_btn")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{paged.map((s) => (
|
{paged.map((s) => (
|
||||||
@@ -56,11 +147,11 @@ export function MyServers() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{servers.length > 0 && (
|
{filtered.length > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
page={page}
|
page={page}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
total={servers.length}
|
total={filtered.length}
|
||||||
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user