import { ArrowDownIcon, ArrowsUpDownIcon, ArrowUpIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon, } from "@heroicons/react/20/solid"; import { useQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import GlobalMap from "@/components/GlobalMap"; import GroupSwitch from "@/components/GroupSwitch"; import { Loader } from "@/components/loading/Loader"; import ServerOverview from "@/components/ServerOverview"; import { ServiceTracker } from "@/components/ServiceTracker"; import VirtualServerList from "@/components/VirtualServerList"; import { SORT_TYPES } from "@/context/sort-context"; import { useSort } from "@/hooks/use-sort"; import { useStatus } from "@/hooks/use-status"; import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { fetchServerGroup, fetchService } from "@/lib/nezha-api"; import { cn } from "@/lib/utils"; import type { NezhaServer, ServerGroup } from "@/types/nezha-api"; type PreparedServer = { online: boolean; server: NezhaServer; }; const EMPTY_SERVER_LIST: NezhaServer[] = []; const isServerOnline = (now: number, server: NezhaServer) => { const lastActiveTime = server.last_active.startsWith("000") ? 0 : Date.parse(server.last_active); return now - lastActiveTime <= 30000; }; const getUsagePercent = (used = 0, total = 0) => { if (!total) return 0; return (used / total) * 100; }; export default function Servers() { const { t } = useTranslation(); const { sortType, sortOrder, setSortOrder, setSortType } = useSort(); const { data: groupData } = useQuery({ queryKey: ["server-group"], queryFn: () => fetchServerGroup(), }); const { data: serviceData } = useQuery({ queryKey: ["service"], queryFn: () => fetchService(), refetchOnMount: true, refetchOnWindowFocus: true, refetchInterval: 10000, }); const hasServices = !!serviceData?.data?.services && Object.keys(serviceData.data.services).length > 0; const { lastData, connected } = useWebSocketContext(); const { status } = useStatus(); const [showServices, setShowServices] = useState("0"); const [showMap, setShowMap] = useState("0"); const [inline, setInline] = useState("0"); const hasRestoredScroll = useRef(false); const [currentGroup, setCurrentGroup] = useState("All"); const nezhaWsData = lastData; const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined; const restoreScrollPosition = useCallback(() => { const isFromMainPage = sessionStorage.getItem("fromMainPage") === "true"; const savedPosition = sessionStorage.getItem("scrollPosition"); const scrollTop = savedPosition ? Number(savedPosition) : Number.NaN; if ( hasRestoredScroll.current || !isFromMainPage || !Number.isFinite(scrollTop) ) { return; } hasRestoredScroll.current = true; requestAnimationFrame(() => { requestAnimationFrame(() => { window.scrollTo({ top: scrollTop, left: 0, behavior: "auto" }); }); }); }, []); const handleTagChange = (newGroup: string) => { setCurrentGroup(newGroup); sessionStorage.setItem("selectedGroup", newGroup); sessionStorage.setItem("scrollPosition", String(window.scrollY || 0)); }; useEffect(() => { const showServicesState = localStorage.getItem("showServices"); if (window.ForceShowServices) { setShowServices("1"); } else if (showServicesState !== null) { setShowServices(showServicesState); } }, []); useEffect(() => { if (!hasServices) { setShowServices("0"); } }, [hasServices]); useEffect(() => { const checkInlineSettings = () => { const isMobile = window.innerWidth < 768; if (!isMobile) { const inlineState = localStorage.getItem("inline"); if (window.ForceCardInline) { setInline("1"); } else if (inlineState !== null) { setInline(inlineState); } } }; checkInlineSettings(); window.addEventListener("resize", checkInlineSettings); return () => { window.removeEventListener("resize", checkInlineSettings); }; }, []); useEffect(() => { const showMapState = localStorage.getItem("showMap"); if (window.ForceShowMap) { setShowMap("1"); } else if (showMapState !== null) { setShowMap(showMapState); } }, []); useEffect(() => { const savedGroup = sessionStorage.getItem("selectedGroup") || "All"; setCurrentGroup(savedGroup); }, []); useEffect(() => { if (nezhaWsData) { restoreScrollPosition(); } }, [nezhaWsData, restoreScrollPosition]); const groupServerIdSets = useMemo(() => { const sets = new Map>(); for (const item of groupData?.data ?? []) { if (Array.isArray(item.servers)) { sets.set(item.group.name, new Set(item.servers)); } } return sets; }, [groupData?.data]); const groupTabs = useMemo( () => [ "All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || []), ], [groupData?.data], ); const { down, downSpeed, filteredServers, offlineServers, onlineServers, totalServers, up, upSpeed, } = useMemo(() => { if (!nezhaWsData) { return { down: 0, downSpeed: 0, filteredServers: EMPTY_SERVER_LIST, offlineServers: 0, onlineServers: 0, totalServers: 0, up: 0, upSpeed: 0, }; } const currentGroupServerIds = groupServerIdSets.get(currentGroup); const groupFilteredServers: PreparedServer[] = []; const overview = { down: 0, downSpeed: 0, offlineServers: 0, onlineServers: 0, up: 0, upSpeed: 0, }; for (const server of nezhaWsData.servers) { if (currentGroup !== "All" && !currentGroupServerIds?.has(server.id)) { continue; } const online = isServerOnline(nezhaWsData.now, server); groupFilteredServers.push({ online, server, }); if (!online) { overview.offlineServers += 1; continue; } overview.onlineServers += 1; overview.up += server.state?.net_out_transfer ?? 0; overview.down += server.state?.net_in_transfer ?? 0; overview.upSpeed += server.state?.net_out_speed ?? 0; overview.downSpeed += server.state?.net_in_speed ?? 0; } const statusFilteredServers = status === "all" ? groupFilteredServers : groupFilteredServers.filter((item) => status === "online" ? item.online : !item.online, ); if (sortType === "default") { const onlineServers: NezhaServer[] = []; const offlineServers: NezhaServer[] = []; for (const item of statusFilteredServers) { if (item.online) { onlineServers.push(item.server); } else { offlineServers.push(item.server); } } return { ...overview, filteredServers: [...onlineServers, ...offlineServers], totalServers: groupFilteredServers.length, }; } const sortedServers = [...statusFilteredServers].sort((a, b) => { if (sortType !== "name") { if (!a.online && b.online) return 1; if (a.online && !b.online) return -1; if (!a.online && !b.online) { return 0; } } let comparison = 0; switch (sortType) { case "name": comparison = a.server.name.localeCompare(b.server.name); break; case "uptime": comparison = (a.server.state?.uptime ?? 0) - (b.server.state?.uptime ?? 0); break; case "system": comparison = (a.server.host?.platform ?? "").localeCompare( b.server.host?.platform ?? "", ); break; case "cpu": comparison = (a.server.state?.cpu ?? 0) - (b.server.state?.cpu ?? 0); break; case "mem": comparison = getUsagePercent( a.server.state?.mem_used, a.server.host?.mem_total, ) - getUsagePercent(b.server.state?.mem_used, b.server.host?.mem_total); break; case "disk": comparison = getUsagePercent( a.server.state?.disk_used, a.server.host?.disk_total, ) - getUsagePercent( b.server.state?.disk_used, b.server.host?.disk_total, ); break; case "up": comparison = (a.server.state?.net_out_speed ?? 0) - (b.server.state?.net_out_speed ?? 0); break; case "down": comparison = (a.server.state?.net_in_speed ?? 0) - (b.server.state?.net_in_speed ?? 0); break; case "up total": comparison = (a.server.state?.net_out_transfer ?? 0) - (b.server.state?.net_out_transfer ?? 0); break; case "down total": comparison = (a.server.state?.net_in_transfer ?? 0) - (b.server.state?.net_in_transfer ?? 0); break; default: comparison = 0; } return sortOrder === "asc" ? comparison : -comparison; }); return { ...overview, filteredServers: sortedServers.map((item) => item.server), totalServers: groupFilteredServers.length, }; }, [ currentGroup, groupServerIdSets, nezhaWsData, sortOrder, sortType, status, ]); if (!connected && !lastData) { return (
{t("info.websocketConnecting")}
); } if (!nezhaWsData) { return (

{t("info.processing")}

); } return (
{hasServices && ( )}
| {t(`sort.types.${sortType.replace(/ /g, "_")}`)}
{showMap === "1" && ( )} {showServices === "1" && }
); }