Dashboard Redesign (#48)

* feat: add user_template setting

* style: header

* style: page padding

* style: header

* feat: header now time

* style: login page

* feat: nav indicator

* style: button inset shadow

* style: footer text size

* feat: header show login_ip

* fix: error toast

* fix: frontend_templates setting

* fix: lint

* feat: pr auto format

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
仓鼠
2024-12-13 23:51:33 +08:00
committed by GitHub
parent b04ef1bb72
commit 8c8d3e3057
132 changed files with 13242 additions and 12878 deletions

View File

@@ -7,137 +7,144 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Terminal } from "@xterm/xterm";
import { AttachAddon } from "@xterm/addon-attach";
import { FitAddon } from '@xterm/addon-fit';
import { useRef, useEffect, useState, useMemo } from "react";
import { sleep } from "@/lib/utils";
import "@xterm/xterm/css/xterm.css";
import { useParams } from 'react-router-dom';
import { Button } from "./ui/button";
import { toast } from "sonner";
import { FMCard } from "./fm";
import useTerminal from "@/hooks/useTerminal";
import { IconButton } from "./xui/icon-button";
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 { useEffect, useMemo, useRef, useState } from "react"
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<React.SetStateAction<boolean>>;
wsUrl: string
setClose: React.Dispatch<React.SetStateAction<boolean>>
}
const XtermComponent: React.FC<XtermProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, setClose, ...props }) => {
const terminalIdRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const XtermComponent: React.FC<XtermProps & JSX.IntrinsicElements["div"]> = ({
wsUrl,
setClose,
...props
}) => {
const terminalIdRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
return () => {
wsRef.current?.close();
terminalRef.current?.dispose();
};
}, []);
wsRef.current?.close()
terminalRef.current?.dispose()
}
}, [])
useEffect(() => {
terminalRef.current = new Terminal({
cursorBlink: true,
fontSize: 16,
});
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.binaryType = "arraybuffer";
})
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
onResize();
onResize()
}
ws.onclose = () => {
terminalRef.current?.dispose();
setClose(true);
terminalRef.current?.dispose()
setClose(true)
}
ws.onerror = (e) => {
console.error(e);
console.error(e)
toast("Websocket error", {
description: "View console for details.",
})
}
}, [wsUrl]);
}, [wsUrl])
const fitAddon = useRef(new FitAddon()).current;
const sendResize = useRef(false);
const fitAddon = useRef(new FitAddon()).current
const sendResize = useRef(false)
const doResize = () => {
if (!terminalIdRef.current) return;
if (!terminalIdRef.current) return
fitAddon.fit();
fitAddon.fit()
const dimensions = fitAddon.proposeDimensions();
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 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);
const msg = new Int8Array(prefix.length + resizeMessage.length)
msg.set(prefix)
msg.set(resizeMessage, prefix.length)
wsRef.current?.send(msg);
wsRef.current?.send(msg)
}
};
}
const onResize = async () => {
if (sendResize.current) return;
if (sendResize.current) return
sendResize.current = true;
sendResize.current = true
try {
await sleep(1500);
doResize();
await sleep(1500)
doResize()
} catch (error) {
console.error('resize error', error);
console.error("resize error", error)
} finally {
sendResize.current = false;
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);
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);
window.removeEventListener("resize", onResize)
if (wsRef.current) {
wsRef.current.close();
wsRef.current.close()
}
};
}, [wsRef.current, terminalRef.current, terminalIdRef.current]);
}
}, [wsRef.current, terminalRef.current, terminalIdRef.current])
return <div ref={terminalIdRef} {...props} />;
};
return <div ref={terminalIdRef} {...props} />
}
export const TerminalPage = () => {
const { id } = useParams<{ id: string }>();
const [open, setOpen] = useState(false);
const terminal = useTerminal(id ? parseInt(id) : undefined);
const { id } = useParams<{ id: string }>()
const [open, setOpen] = useState(false)
const terminal = useTerminal(id ? parseInt(id) : undefined)
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">
{`Terminal (${id})`}
</h1>
<h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1>
<div className="flex-2 flex ml-auto gap-2">
<FMCard id={id} />
</div>
</div>
{terminal?.session_id
?
<XtermComponent className="max-h-[60%] mb-5" wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`} setClose={setOpen} />
:
{terminal?.session_id ? (
<XtermComponent
className="max-h-[60%] mb-5"
wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`}
setClose={setOpen}
/>
) : (
<p>The server does not exist, or have not been connected yet.</p>
}
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
@@ -148,9 +155,7 @@ export const TerminalPage = () => {
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction asChild>
<Button onClick={window.close}>
Close
</Button>
<Button onClick={window.close}>Close</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -161,10 +166,8 @@ export const TerminalPage = () => {
export const TerminalButton = ({ id }: { id: number }) => {
const handleOpenNewTab = () => {
window.open(`/dashboard/terminal/${id}`, '_blank');
};
window.open(`/dashboard/terminal/${id}`, "_blank")
}
return (
<IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
)
return <IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
}