Unverified Commit a9dbc8bc authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(panel): add search and status filtering to my servers page

parent 4ecaf3c6
Loading
Loading
Loading
Loading
+4 −0
Changes for panel/src/i18n/resources/en-US/servers.json: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
{
  "my_servers_title": "My servers",
  "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_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.",
+4 −0
Changes for panel/src/i18n/resources/zh-CN/servers.json: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
{
  "my_servers_title": "我的服务器",
  "my_servers_subtitle": "管理你拥有或可用的服务器。",
  "search_placeholder": "搜索服务器名称或地址…",
  "filter_status_all": "全部状态",
  "search_no_match": "没有匹配的服务器。",
  "search_clear_btn": "清除筛选",
  "no_servers_linked": "暂无关联的服务器",
  "no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。",
  "my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。",
+96 −5
Changes for panel/src/pages/MyServers.tsx: 96 added lines, 5 removed lines.
Original line number Diff line number Diff line
import { useState } from "react";
import { Server } from "lucide-react";
import { Server, Search, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { ServerCard } from "@/components/ServerCard";
import { Pagination } from "@/components/Pagination";
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 { useAsync, useConfig } from "@/lib/hooks";
import { hostFor } from "@/lib/config";

const PAGE_SIZE = 12;

@@ -16,8 +20,29 @@ export function MyServers() {
  const { t } = useTranslation("servers");
  const servers = data ?? [];
  const [page, setPage] = useState(1);
  const totalPages = Math.max(1, Math.ceil(servers.length / PAGE_SIZE));
  const paged = servers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
  const [searchQuery, setSearchQuery] = useState("");
  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 (
    <>
@@ -31,6 +56,56 @@ export function MyServers() {
        </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 />
      ) : error ? (
@@ -42,6 +117,22 @@ export function MyServers() {
          title={t("no_servers_linked")}
          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">
          {paged.map((s) => (
@@ -56,11 +147,11 @@ export function MyServers() {
        </CardContent>
      </Card>

      {servers.length > 0 && (
      {filtered.length > 0 && (
        <Pagination
          page={page}
          pageSize={PAGE_SIZE}
          total={servers.length}
          total={filtered.length}
          onChange={(p) => { setPage(p); if (p > totalPages) setPage(totalPages); }}
        />
      )}