mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-19 09:40:14 +00:00
Implement scroll position management for navigation (#73)
* feat: implement scroll position management for main page navigation * fix: import saveMainPageScrollPosition in ServerCard and ServerCardInline components * feat: save scroll position when navigating to server from DashCommand * fix: prevent restoring stale scroll positions without main page origin
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
import { useCommand } from "@/hooks/use-command";
|
import { useCommand } from "@/hooks/use-command";
|
||||||
import { useTheme } from "@/hooks/use-theme";
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
|
import { saveMainPageScrollPosition } from "@/lib/navigation";
|
||||||
import { formatNezhaInfo } from "@/lib/utils";
|
import { formatNezhaInfo } from "@/lib/utils";
|
||||||
|
|
||||||
export function DashCommand() {
|
export function DashCommand() {
|
||||||
@@ -89,6 +90,7 @@ export function DashCommand() {
|
|||||||
key={server.id}
|
key={server.id}
|
||||||
value={server.name}
|
value={server.name}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
saveMainPageScrollPosition();
|
||||||
navigate(`/server/${server.id}`);
|
navigate(`/server/${server.id}`);
|
||||||
closeCommand();
|
closeCommand();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -59,13 +59,22 @@ export default function GroupSwitch({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTagRef = itemRefs.current[tabs.indexOf(currentTab)];
|
const currentTagRef = itemRefs.current[tabs.indexOf(currentTab)];
|
||||||
|
const scrollContainer = scrollRef.current;
|
||||||
|
|
||||||
if (currentTagRef) {
|
if (currentTagRef && scrollContainer) {
|
||||||
currentTagRef.scrollIntoView({
|
const nextScrollLeft =
|
||||||
|
currentTagRef.offsetLeft -
|
||||||
|
scrollContainer.clientWidth / 2 +
|
||||||
|
currentTagRef.offsetWidth / 2;
|
||||||
|
|
||||||
|
if (typeof scrollContainer.scrollTo === "function") {
|
||||||
|
scrollContainer.scrollTo({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "nearest",
|
left: Math.max(0, nextScrollLeft),
|
||||||
inline: "center",
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
scrollContainer.scrollLeft = Math.max(0, nextScrollLeft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [currentTab, itemRefs, tabs]);
|
}, [currentTab, itemRefs, tabs]);
|
||||||
|
|
||||||
|
|||||||
@@ -207,11 +207,30 @@ type links = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseCustomLinks(customLinks: string | undefined): links[] | null {
|
||||||
|
if (!customLinks) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedLinks = JSON.parse(customLinks);
|
||||||
|
|
||||||
|
if (!Array.isArray(parsedLinks)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedLinks.filter(
|
||||||
|
(link): link is links =>
|
||||||
|
typeof link?.link === "string" && typeof link?.name === "string",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Links() {
|
function Links() {
|
||||||
// @ts-expect-error CustomLinks is a global variable
|
// @ts-expect-error CustomLinks is a global variable
|
||||||
const customLinks = window.CustomLinks as string;
|
const customLinks = window.CustomLinks as string;
|
||||||
|
|
||||||
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
|
const links = parseCustomLinks(customLinks);
|
||||||
|
|
||||||
if (!links) return null;
|
if (!links) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { memo } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import useTooltip from "@/hooks/use-tooltip";
|
import useTooltip from "@/hooks/use-tooltip";
|
||||||
|
import { saveMainPageScrollPosition } from "@/lib/navigation";
|
||||||
|
|
||||||
const MapTooltip = memo(function MapTooltip() {
|
const MapTooltip = memo(function MapTooltip() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -46,7 +47,7 @@ const MapTooltip = memo(function MapTooltip() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sessionStorage.setItem("fromMainPage", "true");
|
saveMainPageScrollPosition();
|
||||||
navigate(`/server/${server.id}`);
|
navigate(`/server/${server.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
GetOsName,
|
GetOsName,
|
||||||
MageMicrosoftWindows,
|
MageMicrosoftWindows,
|
||||||
} from "@/lib/logo-class";
|
} from "@/lib/logo-class";
|
||||||
|
import { saveMainPageScrollPosition } from "@/lib/navigation";
|
||||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||||
import type { NezhaServer } from "@/types/nezha-api";
|
import type { NezhaServer } from "@/types/nezha-api";
|
||||||
import BillingInfo from "./billingInfo";
|
import BillingInfo from "./billingInfo";
|
||||||
@@ -40,7 +41,7 @@ export default function ServerCard({
|
|||||||
} = formatNezhaInfo(now, serverInfo);
|
} = formatNezhaInfo(now, serverInfo);
|
||||||
|
|
||||||
const cardClick = () => {
|
const cardClick = () => {
|
||||||
sessionStorage.setItem("fromMainPage", "true");
|
saveMainPageScrollPosition();
|
||||||
navigate(`/server/${serverInfo.id}`);
|
navigate(`/server/${serverInfo.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
GetOsName,
|
GetOsName,
|
||||||
MageMicrosoftWindows,
|
MageMicrosoftWindows,
|
||||||
} from "@/lib/logo-class";
|
} from "@/lib/logo-class";
|
||||||
|
import { saveMainPageScrollPosition } from "@/lib/navigation";
|
||||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||||
import type { NezhaServer } from "@/types/nezha-api";
|
import type { NezhaServer } from "@/types/nezha-api";
|
||||||
import BillingInfo from "./billingInfo";
|
import BillingInfo from "./billingInfo";
|
||||||
@@ -41,7 +42,7 @@ export default function ServerCardInline({
|
|||||||
} = formatNezhaInfo(now, serverInfo);
|
} = formatNezhaInfo(now, serverInfo);
|
||||||
|
|
||||||
const cardClick = () => {
|
const cardClick = () => {
|
||||||
sessionStorage.setItem("fromMainPage", "true");
|
saveMainPageScrollPosition();
|
||||||
navigate(`/server/${serverInfo.id}`);
|
navigate(`/server/${serverInfo.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ export default function ServerDetailOverview({
|
|||||||
if (previousPath) {
|
if (previousPath) {
|
||||||
setHasHistory(true);
|
setHasHistory(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (previousPath) {
|
||||||
|
sessionStorage.removeItem("fromMainPage");
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { lastData, connected } = useWebSocketContext();
|
const { lastData, connected } = useWebSocketContext();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
|||||||
const totalChecks =
|
const totalChecks =
|
||||||
serviceData.up.reduce((a, b) => a + b, 0) +
|
serviceData.up.reduce((a, b) => a + b, 0) +
|
||||||
serviceData.down.reduce((a, b) => a + b, 0);
|
serviceData.down.reduce((a, b) => a + b, 0);
|
||||||
const uptime = (totalUp / totalChecks) * 100;
|
const uptime = totalChecks > 0 ? (totalUp / totalChecks) * 100 : 0;
|
||||||
|
|
||||||
const avgDelay =
|
const avgDelay =
|
||||||
serviceData.delay.length > 0
|
serviceData.delay.length > 0
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function saveMainPageScrollPosition() {
|
||||||
|
const scrollPosition =
|
||||||
|
window.scrollY ||
|
||||||
|
document.documentElement.scrollTop ||
|
||||||
|
document.body.scrollTop ||
|
||||||
|
0;
|
||||||
|
|
||||||
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
|
sessionStorage.setItem("scrollPosition", String(scrollPosition));
|
||||||
|
}
|
||||||
@@ -51,7 +51,6 @@ export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
|
|||||||
) {
|
) {
|
||||||
lastestRefreshTokenAt = Date.now();
|
lastestRefreshTokenAt = Date.now();
|
||||||
const csrfToken = getCsrfToken();
|
const csrfToken = getCsrfToken();
|
||||||
console.log("Refreshing token with CSRF token:", csrfToken);
|
|
||||||
fetch("/api/v1/refresh-token", {
|
fetch("/api/v1/refresh-token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
+29
-19
@@ -46,8 +46,9 @@ export default function Servers() {
|
|||||||
const [showServices, setShowServices] = useState<string>("0");
|
const [showServices, setShowServices] = useState<string>("0");
|
||||||
const [showMap, setShowMap] = useState<string>("0");
|
const [showMap, setShowMap] = useState<string>("0");
|
||||||
const [inline, setInline] = useState<string>("0");
|
const [inline, setInline] = useState<string>("0");
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const hasRestoredScroll = useRef(false);
|
||||||
const [currentGroup, setCurrentGroup] = useState<string>("All");
|
const [currentGroup, setCurrentGroup] = useState<string>("All");
|
||||||
|
const nezhaWsData = lastData;
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
(window.CustomBackgroundImage as string) !== ""
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
@@ -55,19 +56,30 @@ export default function Servers() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const restoreScrollPosition = useCallback(() => {
|
const restoreScrollPosition = useCallback(() => {
|
||||||
|
const isFromMainPage = sessionStorage.getItem("fromMainPage") === "true";
|
||||||
const savedPosition = sessionStorage.getItem("scrollPosition");
|
const savedPosition = sessionStorage.getItem("scrollPosition");
|
||||||
if (savedPosition && containerRef.current) {
|
const scrollTop = savedPosition ? Number(savedPosition) : Number.NaN;
|
||||||
containerRef.current.scrollTop = Number(savedPosition);
|
|
||||||
|
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) => {
|
const handleTagChange = (newGroup: string) => {
|
||||||
setCurrentGroup(newGroup);
|
setCurrentGroup(newGroup);
|
||||||
sessionStorage.setItem("selectedGroup", newGroup);
|
sessionStorage.setItem("selectedGroup", newGroup);
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem("scrollPosition", String(window.scrollY || 0));
|
||||||
"scrollPosition",
|
|
||||||
String(containerRef.current?.scrollTop || 0),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -120,11 +132,13 @@ export default function Servers() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedGroup = sessionStorage.getItem("selectedGroup") || "All";
|
const savedGroup = sessionStorage.getItem("selectedGroup") || "All";
|
||||||
setCurrentGroup(savedGroup);
|
setCurrentGroup(savedGroup);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nezhaWsData) {
|
||||||
restoreScrollPosition();
|
restoreScrollPosition();
|
||||||
}, [restoreScrollPosition]);
|
}
|
||||||
|
}, [nezhaWsData, restoreScrollPosition]);
|
||||||
const nezhaWsData = lastData;
|
|
||||||
|
|
||||||
const groupTabs = [
|
const groupTabs = [
|
||||||
"All",
|
"All",
|
||||||
@@ -250,7 +264,9 @@ export default function Servers() {
|
|||||||
comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0);
|
comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0);
|
||||||
break;
|
break;
|
||||||
case "system":
|
case "system":
|
||||||
comparison = a.host.platform.localeCompare(b.host.platform);
|
comparison = (a.host?.platform ?? "").localeCompare(
|
||||||
|
b.host?.platform ?? "",
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "cpu":
|
case "cpu":
|
||||||
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0);
|
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0);
|
||||||
@@ -433,10 +449,7 @@ export default function Servers() {
|
|||||||
)}
|
)}
|
||||||
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
||||||
{inline === "1" && (
|
{inline === "1" && (
|
||||||
<section
|
<section className="flex flex-col gap-2 overflow-x-scroll p-px scrollbar-hidden mt-6 server-inline-list">
|
||||||
ref={containerRef}
|
|
||||||
className="flex flex-col gap-2 overflow-x-scroll p-px scrollbar-hidden mt-6 server-inline-list"
|
|
||||||
>
|
|
||||||
{filteredServers.map((serverInfo) => (
|
{filteredServers.map((serverInfo) => (
|
||||||
<ServerCardInline
|
<ServerCardInline
|
||||||
now={nezhaWsData.now}
|
now={nezhaWsData.now}
|
||||||
@@ -447,10 +460,7 @@ export default function Servers() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{inline === "0" && (
|
{inline === "0" && (
|
||||||
<section
|
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list">
|
||||||
ref={containerRef}
|
|
||||||
className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list"
|
|
||||||
>
|
|
||||||
{filteredServers.map((serverInfo) => (
|
{filteredServers.map((serverInfo) => (
|
||||||
<ServerCard
|
<ServerCard
|
||||||
now={nezhaWsData.now}
|
now={nezhaWsData.now}
|
||||||
|
|||||||
@@ -83,8 +83,14 @@ describe("DashCommand", () => {
|
|||||||
expect(screen.getByText("edge-7")).toBeInTheDocument();
|
expect(screen.getByText("edge-7")).toBeInTheDocument();
|
||||||
expect(screen.getByText("ToggleDarkMode")).toBeInTheDocument();
|
expect(screen.getByText("ToggleDarkMode")).toBeInTheDocument();
|
||||||
|
|
||||||
|
Object.defineProperty(window, "scrollY", {
|
||||||
|
configurable: true,
|
||||||
|
value: 256,
|
||||||
|
});
|
||||||
await user.click(screen.getByText("edge-7"));
|
await user.click(screen.getByText("edge-7"));
|
||||||
expect(dashMocks.navigate).toHaveBeenCalledWith("/server/7");
|
expect(dashMocks.navigate).toHaveBeenCalledWith("/server/7");
|
||||||
|
expect(sessionStorage.getItem("fromMainPage")).toBe("true");
|
||||||
|
expect(sessionStorage.getItem("scrollPosition")).toBe("256");
|
||||||
expect(dashMocks.closeCommand).toHaveBeenCalled();
|
expect(dashMocks.closeCommand).toHaveBeenCalled();
|
||||||
|
|
||||||
await user.click(screen.getByText("ToggleDarkMode"));
|
await user.click(screen.getByText("ToggleDarkMode"));
|
||||||
|
|||||||
@@ -179,6 +179,19 @@ describe("Header", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores invalid custom links instead of crashing", async () => {
|
||||||
|
Object.assign(window, {
|
||||||
|
CustomLinks: "{bad-json",
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
expect(await screen.findByText("Nezha")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("link", { name: "Docs" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("stores and removes the active custom background", async () => {
|
it("stores and removes the active custom background", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
headerMocks.backgroundImage = "/desktop.png";
|
headerMocks.backgroundImage = "/desktop.png";
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ describe("Tab and group switches", () => {
|
|||||||
it("scrolls overflowing group tabs and applies custom background styling", async () => {
|
it("scrolls overflowing group tabs and applies custom background styling", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const setCurrentTab = vi.fn();
|
const setCurrentTab = vi.fn();
|
||||||
|
const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
|
||||||
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockReturnValue(320);
|
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockReturnValue(320);
|
||||||
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(120);
|
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(120);
|
||||||
window.CustomBackgroundImage = "/background.jpg";
|
window.CustomBackgroundImage = "/background.jpg";
|
||||||
@@ -125,6 +126,7 @@ describe("Tab and group switches", () => {
|
|||||||
|
|
||||||
expect(scrollContainer.scrollLeft).toBe(42);
|
expect(scrollContainer.scrollLeft).toBe(42);
|
||||||
expect(setCurrentTab).toHaveBeenCalledWith("Asia");
|
expect(setCurrentTab).toHaveBeenCalledWith("Asia");
|
||||||
|
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
container.querySelector(".relative.flex.items-center")?.className,
|
container.querySelector(".relative.flex.items-center")?.className,
|
||||||
).toContain("bg-stone-100/70");
|
).toContain("bg-stone-100/70");
|
||||||
|
|||||||
@@ -72,9 +72,14 @@ describe("ServerCard", () => {
|
|||||||
screen.getAllByText(/billingInfo.remaining: 16/).length,
|
screen.getAllByText(/billingInfo.remaining: 16/).length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
Object.defineProperty(window, "scrollY", {
|
||||||
|
configurable: true,
|
||||||
|
value: 432,
|
||||||
|
});
|
||||||
fireEvent.click(screen.getByText("edge-online"));
|
fireEvent.click(screen.getByText("edge-online"));
|
||||||
|
|
||||||
expect(sessionStorage.getItem("fromMainPage")).toBe("true");
|
expect(sessionStorage.getItem("fromMainPage")).toBe("true");
|
||||||
|
expect(sessionStorage.getItem("scrollPosition")).toBe("432");
|
||||||
expect(screen.getByText("/server/7")).toBeInTheDocument();
|
expect(screen.getByText("/server/7")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ describe("ServerDetailOverview", () => {
|
|||||||
|
|
||||||
it("renders server identity, hardware, traffic, and temperature details", async () => {
|
it("renders server identity, hardware, traffic, and temperature details", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
seedWebSocketData({
|
seedWebSocketData({
|
||||||
server: createServer({
|
server: createServer({
|
||||||
id: 7,
|
id: 7,
|
||||||
@@ -113,8 +114,8 @@ describe("ServerDetailOverview", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
const { unmount } = render(
|
||||||
<MemoryRouter initialEntries={["/server/7"]}>
|
<MemoryRouter initialEntries={["/", "/server/7"]} initialIndex={1}>
|
||||||
<ServerDetailOverview server_id="7" />
|
<ServerDetailOverview server_id="7" />
|
||||||
<LocationProbe />
|
<LocationProbe />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -139,5 +140,8 @@ describe("ServerDetailOverview", () => {
|
|||||||
|
|
||||||
await user.click(screen.getByText("edge-detail"));
|
await user.click(screen.getByText("edge-detail"));
|
||||||
expect(screen.getByText("/")).toBeInTheDocument();
|
expect(screen.getByText("/")).toBeInTheDocument();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
expect(sessionStorage.getItem("fromMainPage")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -93,6 +93,31 @@ describe("ServiceTracker", () => {
|
|||||||
expect(screen.getByText("Monthly")).toBeInTheDocument();
|
expect(screen.getByText("Monthly")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("hidden-server")).not.toBeInTheDocument();
|
expect(screen.queryByText("hidden-server")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to zero uptime when a service has no checks", async () => {
|
||||||
|
apiMocks.fetchService.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
services: {
|
||||||
|
http: {
|
||||||
|
service_name: "HTTP Ping",
|
||||||
|
current_up: 0,
|
||||||
|
current_down: 0,
|
||||||
|
total_up: 0,
|
||||||
|
total_down: 0,
|
||||||
|
delay: [],
|
||||||
|
up: [0, 0],
|
||||||
|
down: [0, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithQuery(<ServiceTracker serverList={[createServer()]} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("HTTP Ping")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("0.0% serviceTracker.uptime")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CycleTransferStatsCard", () => {
|
describe("CycleTransferStatsCard", () => {
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ describe("nezha api fetchers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes the token when a logged-in browser session has cookies", async () => {
|
it("refreshes the token when a logged-in browser session has cookies", async () => {
|
||||||
|
const consoleLog = vi
|
||||||
|
.spyOn(console, "log")
|
||||||
|
.mockImplementation(() => undefined);
|
||||||
Object.defineProperty(document, "cookie", {
|
Object.defineProperty(document, "cookie", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: "nezha_token=token; nz-csrf=test-csrf-token",
|
value: "nezha_token=token; nz-csrf=test-csrf-token",
|
||||||
@@ -108,5 +111,6 @@ describe("nezha api fetchers", () => {
|
|||||||
"X-CSRF-Token": "test-csrf-token",
|
"X-CSRF-Token": "test-csrf-token",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(consoleLog).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -275,6 +275,73 @@ describe("Servers page", () => {
|
|||||||
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
|
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sorts by system even when a platform value is missing", async () => {
|
||||||
|
const missingPlatform = createServer({
|
||||||
|
id: 1,
|
||||||
|
name: "alpha",
|
||||||
|
host: { platform: undefined },
|
||||||
|
});
|
||||||
|
const linux = createServer({
|
||||||
|
id: 2,
|
||||||
|
name: "beta",
|
||||||
|
host: { platform: "linux" },
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([missingPlatform, linux]),
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.selectOptions(screen.getByLabelText("Sort metric"), "system");
|
||||||
|
|
||||||
|
const cards = screen.getAllByTestId("server-card");
|
||||||
|
expect(cards).toHaveLength(2);
|
||||||
|
expect(cards[0]).toHaveTextContent("beta");
|
||||||
|
expect(cards[1]).toHaveTextContent("alpha");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores the saved main page scroll position after data is ready", async () => {
|
||||||
|
const scrollTo = vi.fn();
|
||||||
|
vi.stubGlobal("scrollTo", scrollTo);
|
||||||
|
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||||
|
callback(0);
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
|
sessionStorage.setItem("scrollPosition", "345");
|
||||||
|
|
||||||
|
renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(scrollTo).toHaveBeenCalledWith({
|
||||||
|
top: 345,
|
||||||
|
left: 0,
|
||||||
|
behavior: "auto",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not restore stale scroll positions without a main page origin", () => {
|
||||||
|
const scrollTo = vi.fn();
|
||||||
|
vi.stubGlobal("scrollTo", scrollTo);
|
||||||
|
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||||
|
callback(0);
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
sessionStorage.setItem("scrollPosition", "345");
|
||||||
|
|
||||||
|
renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scrollTo).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("toggles map and service tracker controls when service data exists", async () => {
|
it("toggles map and service tracker controls when service data exists", async () => {
|
||||||
apiMocks.fetchService.mockResolvedValue({
|
apiMocks.fetchService.mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user