mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-19 09:40:14 +00:00
feat: enhance server handling and add empty state messaging in Server component
This commit is contained in:
@@ -114,11 +114,16 @@ function updateServerMetrics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function websocketPayload() {
|
function websocketPayload() {
|
||||||
return JSON.stringify({
|
const payload = { now: Date.now() };
|
||||||
now: Date.now(),
|
|
||||||
online: onlineCount,
|
if (onlineCount > 0) {
|
||||||
servers,
|
payload.online = onlineCount;
|
||||||
});
|
}
|
||||||
|
if (servers.length > 0) {
|
||||||
|
payload.servers = servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsonResponse(response, data) {
|
function jsonResponse(response, data) {
|
||||||
@@ -130,7 +135,7 @@ function jsonResponse(response, data) {
|
|||||||
response.end(JSON.stringify(data));
|
response.end(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverGroups = [
|
const populatedServerGroups = [
|
||||||
{
|
{
|
||||||
group: {
|
group: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -166,6 +171,7 @@ const serverGroups = [
|
|||||||
.map((server) => server.id),
|
.map((server) => server.id),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const serverGroups = serverCount === 0 ? [] : populatedServerGroups;
|
||||||
|
|
||||||
const serviceHistory = Array.from({ length: 30 }, (_, index) => ({
|
const serviceHistory = Array.from({ length: 30 }, (_, index) => ({
|
||||||
up: 95 + (index % 6),
|
up: 95 + (index % 6),
|
||||||
@@ -173,6 +179,22 @@ const serviceHistory = Array.from({ length: 30 }, (_, index) => ({
|
|||||||
delay: 20 + index * 3,
|
delay: 20 + index * 3,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const services =
|
||||||
|
serverCount === 0
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
mock_http: {
|
||||||
|
service_name: "Mock HTTP",
|
||||||
|
current_up: 1,
|
||||||
|
current_down: 0,
|
||||||
|
total_up: 2999,
|
||||||
|
total_down: 1,
|
||||||
|
delay: serviceHistory.map((item) => item.delay),
|
||||||
|
up: serviceHistory.map((item) => item.up),
|
||||||
|
down: serviceHistory.map((item) => item.down),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const httpServer = http.createServer((request, response) => {
|
const httpServer = http.createServer((request, response) => {
|
||||||
const requestUrl = new URL(
|
const requestUrl = new URL(
|
||||||
request.url || "/",
|
request.url || "/",
|
||||||
@@ -213,18 +235,7 @@ const httpServer = http.createServer((request, response) => {
|
|||||||
jsonResponse(response, {
|
jsonResponse(response, {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
services: {
|
services,
|
||||||
mock_http: {
|
|
||||||
service_name: "Mock HTTP",
|
|
||||||
current_up: 1,
|
|
||||||
current_down: 0,
|
|
||||||
total_up: 2999,
|
|
||||||
total_down: 1,
|
|
||||||
delay: serviceHistory.map((item) => item.delay),
|
|
||||||
up: serviceHistory.map((item) => item.up),
|
|
||||||
down: serviceHistory.map((item) => item.down),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cycle_transfer_stats: {},
|
cycle_transfer_stats: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,30 @@ interface WebSocketProviderProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawNezhaWebsocketResponse = Omit<
|
||||||
|
Partial<NezhaWebsocketResponse>,
|
||||||
|
"servers"
|
||||||
|
> & {
|
||||||
|
servers?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeWebSocketResponse(data: unknown): NezhaWebsocketResponse {
|
||||||
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
||||||
|
throw new TypeError("WebSocket message must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = data as RawNezhaWebsocketResponse;
|
||||||
|
if (typeof response.now !== "number" || !Number.isFinite(response.now)) {
|
||||||
|
throw new TypeError("WebSocket message must include a finite now value");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
now: response.now,
|
||||||
|
servers: Array.isArray(response.servers) ? response.servers : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
||||||
url,
|
url,
|
||||||
children,
|
children,
|
||||||
@@ -92,7 +116,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
|||||||
throw new Error("WebSocket message data must be a string");
|
throw new Error("WebSocket message data must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData = JSON.parse(event.data) as NezhaWebsocketResponse;
|
const newData = normalizeWebSocketResponse(JSON.parse(event.data));
|
||||||
setLastData(newData);
|
setLastData(newData);
|
||||||
// 更新历史消息,保持最新的30条记录
|
// 更新历史消息,保持最新的30条记录
|
||||||
setMessageHistory((prev) => {
|
setMessageHistory((prev) => {
|
||||||
|
|||||||
@@ -83,7 +83,9 @@
|
|||||||
"websocketConnecting": "WebSocket verbindet",
|
"websocketConnecting": "WebSocket verbindet",
|
||||||
"websocketConnected": "WebSocket verbunden",
|
"websocketConnected": "WebSocket verbunden",
|
||||||
"websocketDisconnected": "WebSocket getrennt",
|
"websocketDisconnected": "WebSocket getrennt",
|
||||||
"processing": "Verarbeiten..."
|
"processing": "Verarbeiten...",
|
||||||
|
"noServers": "Keine Server verfügbar",
|
||||||
|
"noMatchingServers": "Keine Server entsprechen den aktuellen Filtern"
|
||||||
},
|
},
|
||||||
"tabSwitch": {
|
"tabSwitch": {
|
||||||
"Network": "Netzwerk",
|
"Network": "Netzwerk",
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"websocketConnecting": "WebSocket connecting",
|
"websocketConnecting": "WebSocket connecting",
|
||||||
"websocketConnected": "WebSocket connected",
|
"websocketConnected": "WebSocket connected",
|
||||||
"websocketDisconnected": "WebSocket disconnected",
|
"websocketDisconnected": "WebSocket disconnected",
|
||||||
"processing": "Processing..."
|
"processing": "Processing...",
|
||||||
|
"noServers": "No servers available",
|
||||||
|
"noMatchingServers": "No servers match the current filters"
|
||||||
},
|
},
|
||||||
"serverOverview": {
|
"serverOverview": {
|
||||||
"totalServers": "Total Servers",
|
"totalServers": "Total Servers",
|
||||||
|
|||||||
@@ -127,7 +127,9 @@
|
|||||||
"websocketConnecting": "Conexión WebSocket",
|
"websocketConnecting": "Conexión WebSocket",
|
||||||
"websocketDisconnected": "WebSocket desconectado",
|
"websocketDisconnected": "WebSocket desconectado",
|
||||||
"websocketConnected": "WebSocket conectado",
|
"websocketConnected": "WebSocket conectado",
|
||||||
"processing": "Procesando..."
|
"processing": "Procesando...",
|
||||||
|
"noServers": "No hay servidores disponibles",
|
||||||
|
"noMatchingServers": "Ningún servidor coincide con los filtros actuales"
|
||||||
},
|
},
|
||||||
"NoResults": "No se encontraron resultados.",
|
"NoResults": "No se encontraron resultados.",
|
||||||
"refreshing": "Actualizando",
|
"refreshing": "Actualizando",
|
||||||
|
|||||||
@@ -53,7 +53,9 @@
|
|||||||
"websocketConnecting": "WebSocket подключение",
|
"websocketConnecting": "WebSocket подключение",
|
||||||
"websocketConnected": "WebSocket подключен",
|
"websocketConnected": "WebSocket подключен",
|
||||||
"websocketDisconnected": "WebSocket отключен",
|
"websocketDisconnected": "WebSocket отключен",
|
||||||
"processing": "Обработка..."
|
"processing": "Обработка...",
|
||||||
|
"noServers": "Нет доступных серверов",
|
||||||
|
"noMatchingServers": "Нет серверов, соответствующих текущим фильтрам"
|
||||||
},
|
},
|
||||||
"cycleTransfer": {
|
"cycleTransfer": {
|
||||||
"nextUpdate": "следующее обновление",
|
"nextUpdate": "следующее обновление",
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
"websocketConnecting": "வெப்சாக்கெட் இணைத்தல்",
|
"websocketConnecting": "வெப்சாக்கெட் இணைத்தல்",
|
||||||
"websocketConnected": "வெப்சாக்கெட் இணைக்கப்பட்டுள்ளது",
|
"websocketConnected": "வெப்சாக்கெட் இணைக்கப்பட்டுள்ளது",
|
||||||
"websocketDisconnected": "வெப்சாக்கெட் துண்டிக்கப்பட்டது",
|
"websocketDisconnected": "வெப்சாக்கெட் துண்டிக்கப்பட்டது",
|
||||||
"processing": "செயலாக்கம் ..."
|
"processing": "செயலாக்கம் ...",
|
||||||
|
"noServers": "சேவையகங்கள் எதுவும் இல்லை",
|
||||||
|
"noMatchingServers": "தற்போதைய வடிப்பான்களுடன் பொருந்தும் சேவையகங்கள் எதுவும் இல்லை"
|
||||||
},
|
},
|
||||||
"serverOverview": {
|
"serverOverview": {
|
||||||
"totalServers": "மொத்த சேவையகங்கள்",
|
"totalServers": "மொத்த சேவையகங்கள்",
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"websocketConnecting": "WebSocket 连接中",
|
"websocketConnecting": "WebSocket 连接中",
|
||||||
"websocketConnected": "WebSocket 连接成功",
|
"websocketConnected": "WebSocket 连接成功",
|
||||||
"websocketDisconnected": "WebSocket 连接断开",
|
"websocketDisconnected": "WebSocket 连接断开",
|
||||||
"processing": "处理中..."
|
"processing": "处理中...",
|
||||||
|
"noServers": "暂无服务器",
|
||||||
|
"noMatchingServers": "没有符合当前筛选条件的服务器"
|
||||||
},
|
},
|
||||||
"serverOverview": {
|
"serverOverview": {
|
||||||
"totalServers": "服务器总数",
|
"totalServers": "服务器总数",
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"websocketConnecting": "WebSocket 連線中",
|
"websocketConnecting": "WebSocket 連線中",
|
||||||
"websocketConnected": "WebSocket 連線成功",
|
"websocketConnected": "WebSocket 連線成功",
|
||||||
"websocketDisconnected": "WebSocket 連線中斷",
|
"websocketDisconnected": "WebSocket 連線中斷",
|
||||||
"processing": "處理中..."
|
"processing": "處理中...",
|
||||||
|
"noServers": "暫無伺服器",
|
||||||
|
"noMatchingServers": "沒有符合目前篩選條件的伺服器"
|
||||||
},
|
},
|
||||||
"serverOverview": {
|
"serverOverview": {
|
||||||
"totalServers": "總伺服器",
|
"totalServers": "總伺服器",
|
||||||
|
|||||||
+37
-7
@@ -4,6 +4,7 @@ import {
|
|||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
ChartBarSquareIcon,
|
ChartBarSquareIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
|
ServerStackIcon,
|
||||||
ViewColumnsIcon,
|
ViewColumnsIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -72,6 +73,25 @@ function BackendErrorState({ error }: { error: unknown }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServerEmptyState({ filtered = false }: { filtered?: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="server-empty-state mt-6 flex min-h-52 flex-col items-center justify-center gap-2 px-4 text-center"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<ServerStackIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-7 text-stone-300 dark:text-stone-600"
|
||||||
|
/>
|
||||||
|
<p className="text-sm font-medium text-stone-500 dark:text-stone-400">
|
||||||
|
{t(filtered ? "info.noMatchingServers" : "info.noServers")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Servers({
|
export default function Servers({
|
||||||
backendError,
|
backendError,
|
||||||
}: {
|
}: {
|
||||||
@@ -409,6 +429,8 @@ export default function Servers({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasServers = nezhaWsData.servers.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-0">
|
<div className="mx-auto w-full max-w-5xl px-0">
|
||||||
<ServerOverview
|
<ServerOverview
|
||||||
@@ -420,7 +442,10 @@ export default function Servers({
|
|||||||
upSpeed={upSpeed}
|
upSpeed={upSpeed}
|
||||||
downSpeed={downSpeed}
|
downSpeed={downSpeed}
|
||||||
/>
|
/>
|
||||||
<div className="flex mt-6 items-center justify-between gap-2 server-overview-controls">
|
<div
|
||||||
|
hidden={!hasServers}
|
||||||
|
className="flex mt-6 items-center justify-between gap-2 server-overview-controls"
|
||||||
|
>
|
||||||
<section className="flex items-center gap-2 w-full overflow-hidden">
|
<section className="flex items-center gap-2 w-full overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -546,18 +571,23 @@ export default function Servers({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showMap === "1" && (
|
{hasServers && showMap === "1" && (
|
||||||
<GlobalMap
|
<GlobalMap now={nezhaWsData.now} serverList={nezhaWsData.servers} />
|
||||||
now={nezhaWsData.now}
|
|
||||||
serverList={nezhaWsData?.servers || []}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
{hasServers && showServices === "1" && (
|
||||||
|
<ServiceTracker serverList={filteredServers} />
|
||||||
|
)}
|
||||||
|
{!hasServers ? (
|
||||||
|
<ServerEmptyState />
|
||||||
|
) : filteredServers.length > 0 ? (
|
||||||
<VirtualServerList
|
<VirtualServerList
|
||||||
inline={inline === "1"}
|
inline={inline === "1"}
|
||||||
now={nezhaWsData.now}
|
now={nezhaWsData.now}
|
||||||
servers={filteredServers}
|
servers={filteredServers}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ServerEmptyState filtered />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ function WebSocketProbe() {
|
|||||||
<div>
|
<div>
|
||||||
<p>{connected ? "connected" : "disconnected"}</p>
|
<p>{connected ? "connected" : "disconnected"}</p>
|
||||||
<p>{lastData?.servers[0]?.name ?? "none"}</p>
|
<p>{lastData?.servers[0]?.name ?? "none"}</p>
|
||||||
<p>{messageHistory.length}</p>
|
<p data-testid="message-count">{messageHistory.length}</p>
|
||||||
|
<p data-testid="history-server-count">
|
||||||
|
{messageHistory.reduce((total, item) => total + item.servers.length, 0)}
|
||||||
|
</p>
|
||||||
<p>{needReconnect ? "needs-reconnect" : "stable"}</p>
|
<p>{needReconnect ? "needs-reconnect" : "stable"}</p>
|
||||||
<button type="button" onClick={() => setNeedReconnect(true)}>
|
<button type="button" onClick={() => setNeedReconnect(true)}>
|
||||||
mark
|
mark
|
||||||
@@ -268,7 +271,24 @@ describe("WebSocketProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("second")).toBeInTheDocument();
|
expect(screen.getByText("second")).toBeInTheDocument();
|
||||||
expect(screen.getByText("2")).toBeInTheDocument();
|
expect(screen.getByTestId("message-count")).toHaveTextContent("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes missing and non-array server collections", () => {
|
||||||
|
renderWebSocketProvider(<WebSocketProbe />);
|
||||||
|
const socket = FakeWebSocket.instances[0];
|
||||||
|
const now = Date.parse("2025-01-01T00:00:20.000Z");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
socket.open();
|
||||||
|
socket.message(JSON.stringify({ now }));
|
||||||
|
socket.message(JSON.stringify({ now, servers: null }));
|
||||||
|
socket.message(JSON.stringify({ now, servers: { invalid: true } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("none")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("message-count")).toHaveTextContent("3");
|
||||||
|
expect(screen.getByTestId("history-server-count")).toHaveTextContent("0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps only the latest thirty websocket messages", () => {
|
it("keeps only the latest thirty websocket messages", () => {
|
||||||
@@ -283,7 +303,7 @@ describe("WebSocketProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("message-30")).toBeInTheDocument();
|
expect(screen.getByText("message-30")).toBeInTheDocument();
|
||||||
expect(screen.getByText("30")).toBeInTheDocument();
|
expect(screen.getByTestId("message-count")).toHaveTextContent("30");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores malformed websocket messages without disconnecting", () => {
|
it("ignores malformed websocket messages without disconnecting", () => {
|
||||||
@@ -300,13 +320,34 @@ describe("WebSocketProvider", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("connected")).toBeInTheDocument();
|
expect(screen.getByText("connected")).toBeInTheDocument();
|
||||||
expect(screen.getByText("none")).toBeInTheDocument();
|
expect(screen.getByText("none")).toBeInTheDocument();
|
||||||
expect(screen.getByText("0")).toBeInTheDocument();
|
expect(screen.getByTestId("message-count")).toHaveTextContent("0");
|
||||||
expect(consoleError).toHaveBeenCalledWith(
|
expect(consoleError).toHaveBeenCalledWith(
|
||||||
"Failed to parse WebSocket message:",
|
"Failed to parse WebSocket message:",
|
||||||
expect.any(SyntaxError),
|
expect.any(SyntaxError),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores websocket messages without a valid response shape", () => {
|
||||||
|
const consoleError = vi
|
||||||
|
.spyOn(console, "error")
|
||||||
|
.mockImplementation(() => undefined);
|
||||||
|
renderWebSocketProvider(<WebSocketProbe />);
|
||||||
|
const socket = FakeWebSocket.instances[0];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
socket.open();
|
||||||
|
socket.message("null");
|
||||||
|
socket.message(JSON.stringify({ servers: [] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId("message-count")).toHaveTextContent("0");
|
||||||
|
expect(consoleError).toHaveBeenCalledTimes(2);
|
||||||
|
expect(consoleError).toHaveBeenCalledWith(
|
||||||
|
"Failed to parse WebSocket message:",
|
||||||
|
expect.any(TypeError),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("exposes manual reconnect state separately from socket state", async () => {
|
it("exposes manual reconnect state separately from socket state", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderWebSocketProvider(<WebSocketProbe />);
|
renderWebSocketProvider(<WebSocketProbe />);
|
||||||
|
|||||||
@@ -241,6 +241,45 @@ describe("Servers page", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows an empty state and hides controls when there are no servers", () => {
|
||||||
|
const { container } = renderServerPage({
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId("server-overview")).toHaveTextContent("0:0:0");
|
||||||
|
expect(screen.getByText("info.noServers")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
container.querySelector(".server-overview-controls"),
|
||||||
|
).not.toBeVisible();
|
||||||
|
expect(screen.queryByTestId("server-card")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("global-map")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("service-tracker")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a filtered empty state while keeping controls available", async () => {
|
||||||
|
const offline = createServer({
|
||||||
|
id: 1,
|
||||||
|
name: "offline",
|
||||||
|
last_active: "2024-12-31T23:00:00.000Z",
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const { container } = renderServerPage(
|
||||||
|
{
|
||||||
|
connected: true,
|
||||||
|
lastData: websocketPayload([offline]),
|
||||||
|
},
|
||||||
|
{ withStatusControl: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "online-only" }));
|
||||||
|
|
||||||
|
expect(screen.getByText("info.noMatchingServers")).toBeInTheDocument();
|
||||||
|
expect(container.querySelector(".server-overview-controls")).not.toBeNull();
|
||||||
|
expect(screen.queryByTestId("server-card")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("filters servers by selected group", async () => {
|
it("filters servers by selected group", async () => {
|
||||||
const online = createServer({ id: 1, name: "alpha" });
|
const online = createServer({ id: 1, name: "alpha" });
|
||||||
const offline = createServer({
|
const offline = createServer({
|
||||||
|
|||||||
Reference in New Issue
Block a user