From 8733070cf14b9889b2c25bc7a5cc1a6269e1540c Mon Sep 17 00:00:00 2001 From: Bot Date: Thu, 16 Apr 2026 12:14:30 +0800 Subject: [PATCH] Fix ts configuration and missing fragment in server route after terminal removal --- src/api/terminal.ts | 9 -- src/components/terminal.tsx | 206 ------------------------------------ src/hooks/useTerminal.ts | 23 ---- src/main.tsx | 57 ++++------ src/routes/server.tsx | 146 ++++++++++--------------- 5 files changed, 78 insertions(+), 363 deletions(-) delete mode 100644 src/api/terminal.ts delete mode 100644 src/components/terminal.tsx delete mode 100644 src/hooks/useTerminal.ts diff --git a/src/api/terminal.ts b/src/api/terminal.ts deleted file mode 100644 index 1ad6b73..0000000 --- a/src/api/terminal.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ModelCreateTerminalResponse } from "@/types" - -import { FetcherMethod, fetcher } from "./api" - -export const createTerminal = async (id: number): Promise => { - return fetcher(FetcherMethod.POST, "/api/v1/terminal", { - server_id: id, - }) -} diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx deleted file mode 100644 index 07d0cbb..0000000 --- a/src/components/terminal.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog" -import useTerminal from "@/hooks/useTerminal" -import { sleep } from "@/lib/utils" -import { AttachAddon } from "@xterm/addon-attach" -import { FitAddon } from "@xterm/addon-fit" -import { Terminal } from "@xterm/xterm" -import "@xterm/xterm/css/xterm.css" -import { Terminal as TerminalIcon } from "lucide-react" -import { JSX, forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" -import { toast } from "sonner" - -import { FMCard } from "./fm" -import { Button } from "./ui/button" -import { IconButton } from "./xui/icon-button" - -interface XtermProps { - wsUrl: string - setClose: React.Dispatch> -} - -const XtermComponent = forwardRef( - ({ wsUrl, setClose, ...props }, ref) => { - const terminalIdRef = useRef(null) - const terminalRef = useRef(null) - const wsRef = useRef(null) - - useImperativeHandle(ref, () => { - return { - ...terminalIdRef.current!, - async requestFullscreen() { - await terminalIdRef.current?.requestFullscreen() - }, - } - }, []) - - useEffect(() => { - return () => { - wsRef.current?.close() - terminalRef.current?.dispose() - } - }, []) - - useEffect(() => { - terminalRef.current = new Terminal({ - cursorBlink: true, - fontSize: 16, - }) - const url = new URL(wsUrl, window.location.origin) - url.protocol = url.protocol.replace("http", "ws") - const ws = new WebSocket(url) - wsRef.current = ws - ws.binaryType = "arraybuffer" - ws.onopen = () => { - onResize() - } - ws.onclose = () => { - terminalRef.current?.dispose() - setClose(true) - } - ws.onerror = (e) => { - console.error(e) - toast("Websocket error", { - description: "View console for details.", - }) - } - }, [wsUrl]) - - const fitAddon = useRef(new FitAddon()).current - const sendResize = useRef(false) - - const doResize = () => { - if (!terminalIdRef.current) return - - fitAddon.fit() - - const dimensions = fitAddon.proposeDimensions() - - if (dimensions) { - const prefix = new Int8Array([1]) - const resizeMessage = new TextEncoder().encode( - JSON.stringify({ - Rows: dimensions.rows, - Cols: dimensions.cols, - }), - ) - - const msg = new Int8Array(prefix.length + resizeMessage.length) - msg.set(prefix) - msg.set(resizeMessage, prefix.length) - - wsRef.current?.send(msg) - } - } - - const onResize = async () => { - if (sendResize.current) return - - sendResize.current = true - try { - await sleep(1500) - doResize() - } catch (error) { - console.error("resize error", error) - } finally { - sendResize.current = false - } - } - - useEffect(() => { - if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return - const attachAddon = new AttachAddon(wsRef.current) - terminalRef.current.loadAddon(attachAddon) - terminalRef.current.loadAddon(fitAddon) - terminalRef.current.open(terminalIdRef.current) - window.addEventListener("resize", onResize) - return () => { - window.removeEventListener("resize", onResize) - if (wsRef.current) { - wsRef.current.close() - } - } - }, [wsRef.current, terminalRef.current, terminalIdRef.current]) - - return
- }, -) - -export const TerminalPage = () => { - const { id } = useParams<{ id: string }>() - const [open, setOpen] = useState(false) - const terminal = useTerminal(id ? parseInt(id) : undefined) - const terminalIdRef = useRef(null) - return ( -
-
-

{`Terminal (${id})`}

-
- { - await terminalIdRef.current?.requestFullscreen() - }} - /> - -
-
- {terminal?.session_id ? ( - - ) : ( -

The server does not exist, or have not been connected yet.

- )} - - - - Session completed - - You may close this window now. - - - - - - - - - -
- ) -} - -export const TerminalButton = ({ id, menuItem = false }: { id: number; menuItem?: boolean }) => { - const { t } = useTranslation() - const handleOpenNewTab = () => { - window.open(`/dashboard/terminal/${id}`, "_blank") - } - - if (menuItem) { - return ( - - ) - } - - return -} diff --git a/src/hooks/useTerminal.ts b/src/hooks/useTerminal.ts deleted file mode 100644 index 759395e..0000000 --- a/src/hooks/useTerminal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createTerminal } from "@/api/terminal" -import { ModelCreateTerminalResponse } from "@/types" -import { useEffect, useState } from "react" - -export default function useTerminal(serverId?: number) { - const [terminal, setTerminal] = useState(null) - - async function fetchTerminal() { - try { - const response = await createTerminal(serverId!) - setTerminal(response) - } catch (error) { - console.error("Failed to fetch terminal:", error) - } - } - - useEffect(() => { - if (!serverId) return - fetchTerminal() - }, [serverId]) - - return terminal -} diff --git a/src/main.tsx b/src/main.tsx index efb310a..d7a0305 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,31 +1,27 @@ -// NOTE: Do not modify the import order unless absolutely necessary. import { createRoot } from "react-dom/client" import { RouterProvider, createBrowserRouter } from "react-router-dom" -import "./index.css" -import "./lib/i18n" +import ErrorPage from "./error-page" import { AuthProvider } from "./hooks/useAuth" import { NotificationProvider } from "./hooks/useNotfication" import { ServerProvider } from "./hooks/useServer" - -import Root from "./routes/root" -import ErrorPage from "./error-page" - -import ProtectedRoute from "./routes/protect" -import CronPage from "./routes/cron" -import LoginPage from "./routes/login" -import ServerPage from "./routes/server" -import ServicePage from "./routes/service" -import { TerminalPage } from "./components/terminal" -import DDNSPage from "./routes/ddns" -import NATPage from "./routes/nat" -import NotificationGroupPage from "./routes/notification-group" -import ServerGroupPage from "./routes/server-group" +import "./index.css" +import "./lib/i18n" import AlertRulePage from "./routes/alert-rule" +import CronPage from "./routes/cron" +import DDNSPage from "./routes/ddns" +import LoginPage from "./routes/login" +import NATPage from "./routes/nat" import NotificationPage from "./routes/notification" +import NotificationGroupPage from "./routes/notification-group" import OnlineUserPage from "./routes/online-user" import ProfilePage from "./routes/profile" +import ProtectedRoute from "./routes/protect" +import Root from "./routes/root" +import ServerPage from "./routes/server" +import ServerGroupPage from "./routes/server-group" +import ServicePage from "./routes/service" import SettingsPage from "./routes/settings" import UserPage from "./routes/user" import WAFPage from "./routes/waf" @@ -74,6 +70,14 @@ const router = createBrowserRouter([ ), }, + { + path: "/dashboard/notification", + element: ( + + + + ), + }, { path: "/dashboard/alert-rule", element: ( @@ -106,18 +110,7 @@ const router = createBrowserRouter([ ), }, - { - path: "/dashboard/terminal/:id", - element: , - }, - { - path: "/dashboard/notification", - element: ( - - - - ), - }, + { path: "/dashboard/profile", element: ( @@ -128,11 +121,7 @@ const router = createBrowserRouter([ }, { path: "/dashboard/settings", - element: ( - - - - ), + element: , }, { path: "/dashboard/settings/user", diff --git a/src/routes/server.tsx b/src/routes/server.tsx index 938d46e..07ee154 100644 --- a/src/routes/server.tsx +++ b/src/routes/server.tsx @@ -10,12 +10,6 @@ import { ServerCard } from "@/components/server" import { ServerConfigCard } from "@/components/server-config" import { ServerConfigCardBatch } from "@/components/server-config-batch" import { Checkbox } from "@/components/ui/checkbox" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" import { Table, TableBody, @@ -36,10 +30,7 @@ import useSWR from "swr" export default function ServerPage() { const { t } = useTranslation() - const { data, mutate, error, isLoading } = useSWR("/api/v1/server", swrFetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }) + const { data, mutate, error, isLoading } = useSWR("/api/v1/server", swrFetcher) const { serverGroups } = useServer() useEffect(() => { @@ -93,7 +84,7 @@ export default function ServerPage() { accessorFn: (row) => { return ( serverGroups - ?.filter((sg) => sg.servers?.includes(row.id)) + ?.filter((sg) => sg.servers?.includes(row.id!)) .map((sg) => sg.group.id) || [] ) }, @@ -113,7 +104,7 @@ export default function ServerPage() { { header: t("Version"), accessorKey: "host.version", - accessorFn: (row) => row.host.version || t("Unknown"), + accessorFn: (row) => row.host?.version || t("Unknown"), }, { header: t("EnableDDNS"), @@ -149,30 +140,11 @@ export default function ServerPage() { return ( <> - - - - - - - - - - - - - - - - + ) @@ -193,11 +165,11 @@ export default function ServerPage() { const selectedRows = table.getSelectedRowModel().rows return ( -
-
+
+

{t("Server")}

r.original.id).filter(Boolean) as number[], @@ -207,7 +179,7 @@ export default function ServerPage() { { - const id = selectedRows.map((r) => r.original.id) + const id = selectedRows.map((r) => r.original.id) as number[] if (id.length < 1) { toast(t("Error"), { description: t("Results.SelectAtLeastOneServer"), @@ -240,67 +212,59 @@ export default function ServerPage() { }) }} /> - r.original.id)} /> + r.original.id) as number[]} /> r.original.id)} + sid={selectedRows.map((r) => r.original.id) as number[]} className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg" />
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ) - })} +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ) + })} + + ))} + + + {isLoading ? ( + + + {t("Loading")}... + + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} - ))} - - - {isLoading ? ( - - - {t("Loading")}... - - - ) : table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {t("NoResults")} - - - )} - -
-
+ )) + ) : ( + + + {t("NoResults")} + + + )} + +
) }