mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-19 09:40:14 +00:00
fix(dashboard): fix Total Domains filtering and theme switching, add form attributes
This commit is contained in:
+5
-1
@@ -85,7 +85,11 @@ const MainApp: React.FC = () => {
|
|||||||
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forceTheme === "dark" || forceTheme === "light") {
|
const savedTheme = localStorage.getItem("vite-ui-theme");
|
||||||
|
if (
|
||||||
|
(!savedTheme || savedTheme === "system") &&
|
||||||
|
(forceTheme === "dark" || forceTheme === "light")
|
||||||
|
) {
|
||||||
setTheme(forceTheme);
|
setTheme(forceTheme);
|
||||||
}
|
}
|
||||||
}, [forceTheme, setTheme]);
|
}, [forceTheme, setTheme]);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ export function DashCommand() {
|
|||||||
return (
|
return (
|
||||||
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
|
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
|
id="dash-command-search"
|
||||||
|
name="dashCommandSearch"
|
||||||
placeholder={t("TypeCommand")}
|
placeholder={t("TypeCommand")}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { createContext, type ReactNode, useEffect, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "system" | "scheduled";
|
export type Theme = "dark" | "light" | "system" | "scheduled";
|
||||||
|
|
||||||
@@ -94,13 +101,21 @@ export function ThemeProvider({
|
|||||||
};
|
};
|
||||||
}, [theme, hour, isSystemDark]);
|
}, [theme, hour, isSystemDark]);
|
||||||
|
|
||||||
const value = {
|
const handleSetTheme = useCallback(
|
||||||
theme,
|
(nextTheme: Theme) => {
|
||||||
setTheme: (theme: Theme) => {
|
localStorage.setItem(storageKey, nextTheme);
|
||||||
localStorage.setItem(storageKey, theme);
|
setTheme(nextTheme);
|
||||||
setTheme(theme);
|
|
||||||
},
|
},
|
||||||
};
|
[storageKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
theme,
|
||||||
|
setTheme: handleSetTheme,
|
||||||
|
}),
|
||||||
|
[theme, handleSetTheme],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProviderContext.Provider value={value}>
|
<ThemeProviderContext.Provider value={value}>
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ const CommandInput = React.forwardRef<
|
|||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
id={props.id ?? "command-palette-search"}
|
||||||
|
name={props.name ?? "commandPaletteSearch"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export function initCustomConfig() {
|
|||||||
: "https://loohui.com/wp-content/uploads/images/background_day.jpg";
|
: "https://loohui.com/wp-content/uploads/images/background_day.jpg";
|
||||||
|
|
||||||
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
||||||
window.ForceTheme = isNight ? "dark" : "light";
|
|
||||||
|
|
||||||
/* LOGO / 副标题 / 链接 */
|
/* LOGO / 副标题 / 链接 */
|
||||||
window.CustomLogo = "https://loohui.com/wp-content/uploads/images/pet.png";
|
window.CustomLogo = "https://loohui.com/wp-content/uploads/images/pet.png";
|
||||||
|
|||||||
@@ -136,13 +136,14 @@ export default function Servers({
|
|||||||
queryFn: getDomains,
|
queryFn: getDomains,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当用户点击 "在线" 或 "离线" 或 "总服务器数" 时,status 会改变,我们就自动切回服务器视图
|
const prevStatusRef = useRef(status);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只有在 status 改变时才触发,避免无限循环
|
if (prevStatusRef.current !== status) {
|
||||||
const currentStatus = status || "all";
|
prevStatusRef.current = status;
|
||||||
if (currentStatus !== "all" || activeView === "domains") {
|
if (activeView === "domains") {
|
||||||
setActiveView("servers");
|
setActiveView("servers");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [status, activeView]);
|
}, [status, activeView]);
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
@@ -586,6 +587,8 @@ export default function Servers({
|
|||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
aria-label="Sort metric"
|
aria-label="Sort metric"
|
||||||
|
id="server-sort-metric"
|
||||||
|
name="serverSortMetric"
|
||||||
value={sortType}
|
value={sortType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value as typeof sortType;
|
const val = e.target.value as typeof sortType;
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ describe("App", () => {
|
|||||||
appMocks.backgroundImage = undefined;
|
appMocks.backgroundImage = undefined;
|
||||||
appMocks.fetchSetting.mockResolvedValue(settingResponse());
|
appMocks.fetchSetting.mockResolvedValue(settingResponse());
|
||||||
appMocks.injectContext.mockResolvedValue(undefined);
|
appMocks.injectContext.mockResolvedValue(undefined);
|
||||||
|
appMocks.setTheme.mockClear();
|
||||||
|
window.ForceTheme = "";
|
||||||
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the main shell after settings load and applies global theme/background settings", async () => {
|
it("renders the main shell after settings load and applies global theme/background settings", async () => {
|
||||||
@@ -187,4 +190,16 @@ describe("App", () => {
|
|||||||
|
|
||||||
expect(await screen.findByText("server-detail-page")).toBeInTheDocument();
|
expect(await screen.findByText("server-detail-page")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not overwrite user-selected theme with ForceTheme if user previously saved a theme", async () => {
|
||||||
|
localStorage.setItem("vite-ui-theme", "light");
|
||||||
|
Object.assign(window, {
|
||||||
|
ForceTheme: "dark",
|
||||||
|
});
|
||||||
|
|
||||||
|
renderApp();
|
||||||
|
|
||||||
|
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||||
|
expect(appMocks.setTheme).not.toHaveBeenCalledWith("dark");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,12 +52,32 @@ vi.mock("@/components/ServerOverview", () => ({
|
|||||||
offline,
|
offline,
|
||||||
online,
|
online,
|
||||||
total,
|
total,
|
||||||
|
onViewChange,
|
||||||
}: {
|
}: {
|
||||||
offline: number;
|
offline: number;
|
||||||
online: number;
|
online: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
totalDomains?: number;
|
||||||
|
onViewChange?: (view: "servers" | "domains") => void;
|
||||||
}) => (
|
}) => (
|
||||||
<div data-testid="server-overview">{`${total}:${online}:${offline}`}</div>
|
<div data-testid="server-overview">
|
||||||
|
<span>{`${total}:${online}:${offline}`}</span>
|
||||||
|
{onViewChange && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="switch-to-domains"
|
||||||
|
onClick={() => onViewChange("domains")}
|
||||||
|
>
|
||||||
|
domains-view
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/DomainStatus", () => ({
|
||||||
|
DomainStatus: () => (
|
||||||
|
<div data-testid="domain-status">domain-status-content</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -542,4 +562,35 @@ describe("Servers page", () => {
|
|||||||
expect(screen.getByText("alpha")).toBeInTheDocument();
|
expect(screen.getByText("alpha")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("beta")).not.toBeInTheDocument();
|
expect(screen.queryByText("beta")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches to domains view when Total Domains is selected and does not immediately revert", async () => {
|
||||||
|
const online = createServer({ id: 1, name: "alpha" });
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([online]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId("server-card")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByTestId("switch-to-domains"));
|
||||||
|
|
||||||
|
// Server controls/cards should now be hidden
|
||||||
|
expect(screen.queryByTestId("server-card")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("domain-status")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders sort metric select with id and name attributes", async () => {
|
||||||
|
const online = createServer({ id: 1, name: "alpha" });
|
||||||
|
|
||||||
|
renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([online]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectElement = screen.getByRole("combobox", { name: "Sort metric" });
|
||||||
|
expect(selectElement).toHaveAttribute("id", "server-sort-metric");
|
||||||
|
expect(selectElement).toHaveAttribute("name", "serverSortMetric");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user