diff --git a/.gitignore b/.gitignore index a547bf36..56bf7b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,139 @@ dist-ssr *.njsproj *.sln *.sw? + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.env.local +.env.development.local +.env.test.local +.env.production.local + +.bash_history +.bashrc +.init_done +.profile +.sudo_as_admin_successful +.wget-hsts +.git-credentials +.docker/ +.bash_logout + +# VSCode Files +.vscode-server/ + +# Configs +.config/ + +# .dotnet +.dotnet/ + +# .local +.local/ \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index d5f8b403..5e6c4e2b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,8 +19,8 @@ FROM nginx:alpine RUN apk add --no-cache nodejs npm COPY docker/nginx.conf /etc/nginx/nginx.conf COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html -COPY --from=backend-build /backend /backend -COPY --from=backend-build /backend/entrypoint.sh /backend/entrypoint.sh +COPY --from=backend-build src/backend/ /src/backend/ +COPY --from=backend-build src/backend/entrypoint.sh /src/backend/entrypoint.sh # Configure start-up RUN chmod +x /backend/entrypoint.sh diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 563fa9a0..00000000 --- a/src/.gitignore +++ /dev/null @@ -1,149 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -.bash_history -.bashrc -.init_done -.profile -.sudo_as_admin_successful -.wget-hsts -.git-credentials -.docker/ -.bash_logout - -# VSCode Files -.vscode-server/ - -# Configs -.config/ - -# .dotnet -.dotnet/ - -# .local -.local/ \ No newline at end of file diff --git a/src/App.css b/src/App.css index 5b5bd089..779f3eb4 100644 --- a/src/App.css +++ b/src/App.css @@ -7,8 +7,6 @@ .sidebar { display: flex; - flex-shrink: 1; - flex-grow: 1; flex-direction: column; gap: 0.5em; position: fixed; @@ -24,39 +22,49 @@ .topbar { display: flex; - flex-shrink: 0; - flex-grow: 1; flex-direction: row; position: fixed; - justify-content: space-between; align-items: center; - text-align: center; - padding: 30px; + padding: 8px; background-color: #323232; top: 0; left: 14em; right: 0; - width: calc(100% - 14em); - min-height: 36px; - height: auto; - font-size: 16px; - gap: 0.5em; + height: 48px; overflow-x: auto; + gap: 8px; + border-bottom: 1px solid #404040; } -.topbar button { - padding: 0.5em 1em; - background-color: #444; - border: none; +.tab-item { + display: flex; + align-items: center; + gap: 4px; + background: #404040; border-radius: 4px; - color: white; - cursor: pointer; - white-space: nowrap; + padding: 4px; + flex-shrink: 0; /* Prevent tabs from shrinking */ } -.topbar button.active-tab { - background-color: #1a1a1a; - border-bottom: 2px solid white; +.tab-item button { + padding: 6px 12px; + background: none; + border: none; + color: #fff; + cursor: pointer; +} + +.tab-close { + padding: 2px 6px !important; + border-radius: 50%; +} + +.tab-close:hover { + background: #ff5555 !important; +} + +.active-tab { + background: #1a1a1a !important; } .terminal-tab { @@ -66,6 +74,8 @@ right: 0; bottom: 0; display: none; + width: 100%; + height: 100%; } .terminal-tab.active { @@ -74,8 +84,6 @@ .add-host { display: flex; - flex-shrink: 1; - flex-grow: 1; flex-direction: column; justify-content: space-between; position: fixed; @@ -125,7 +133,7 @@ .terminal-wrapper { position: fixed; - top: 96px; + top: 64px; left: 14em; right: 0; bottom: 0; diff --git a/src/App.jsx b/src/App.jsx index 97c7e3ba..cb3be95f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,6 +31,14 @@ function App() { } }; + const closeTab = (id) => { + const newTerminals = terminals.filter(t => t.id !== id); + setTerminals(newTerminals); + if (activeTab === id) { + setActiveTab(newTerminals[0]?.id || null); + } + }; + return ( <>
@@ -39,13 +47,15 @@ function App() {
{terminals.map((terminal) => ( - +
+ + +
))}
@@ -54,7 +64,7 @@ function App() { key={terminal.id} className={`terminal-tab ${terminal.id === activeTab ? "active" : ""}`} > - {terminal.hostConfig && } + {terminal.hostConfig && }
))} diff --git a/src/Terminal.jsx b/src/Terminal.jsx index ee83b57b..2befc9c5 100644 --- a/src/Terminal.jsx +++ b/src/Terminal.jsx @@ -1,4 +1,3 @@ -// Terminal.jsx import { useEffect, useRef } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; @@ -15,10 +14,15 @@ export function NewTerminal({ hostConfig }) { // Initialize terminal const terminal = new Terminal({ cursorBlink: true, - cursorStyle: "block", - theme: { background: "#1a1a1a", foreground: "#ffffff", cursor: "#ffffff" }, + theme: { + background: "#1a1a1a", + foreground: "#ffffff", + cursor: "#ffffff", + }, fontSize: 14, scrollback: 1000, + rendererType: "canvas", + allowTransparency: true, }); // Initialize FitAddon for auto-sizing @@ -28,20 +32,11 @@ export function NewTerminal({ hostConfig }) { // Open terminal in the container terminal.open(terminalRef.current); - // Apply fit after terminal is fully initialized - setTimeout(() => { - fitAddon.fit(); - resizeTerminal(); - }, 100); - - // Focus on terminal and reset layout - terminal.focus(); - - // Resize terminal to fit the container + // Resize function (Restoring your original logic) const resizeTerminal = () => { const terminalContainer = terminalRef.current; const sidebarWidth = 14 * 16; // Sidebar width in pixels - const topbarHeight = 96; // Topbar height in pixels + const topbarHeight = 64; // Topbar height in pixels const availableWidth = window.innerWidth - sidebarWidth; const availableHeight = window.innerHeight - topbarHeight; @@ -51,42 +46,49 @@ export function NewTerminal({ hostConfig }) { fitAddon.fit(); const { cols, rows } = terminal; - // Emit new terminal size to the backend if (socket) { socket.emit("resize", { cols, rows }); console.log(`Terminal resized: cols=${cols}, rows=${rows}`); } }; - // Handle window resize events + // Ensure correct sizing on start + setTimeout(() => { + fitAddon.fit(); + resizeTerminal(); + }, 50); // Small delay to ensure proper initialization + + // Focus on terminal after initialization + terminal.focus(); + + // Listen for window resize events window.addEventListener("resize", resizeTerminal); // Write initial connection message terminal.write("\r\n*** Connecting to backend ***\r\n"); - // Create the socket connection with the provided hostConfig - const socket = io("http://localhost:8081"); + // Create socket connection + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/`; - // Emit the hostConfig to the server to start SSH connection - fitAddon.fit(); - const { cols, rows } = terminal; - socket.emit("connectToHost", cols, rows, hostConfig); + const socket = io(wsUrl); - // Handle socket connection events + // Emit hostConfig to start SSH connection socket.on("connect", () => { + fitAddon.fit(); + resizeTerminal(); // Ensure proper size on connection + const { cols, rows } = terminal; + socket.emit("connectToHost", cols, rows, hostConfig); terminal.write("\r\n*** Connected to backend ***\r\n"); - // Send keystrokes to the backend terminal.onKey((key) => { socket.emit("data", key.key); }); - // Display output from the backend socket.on("data", (data) => { terminal.write(data); }); - // Handle disconnection socket.on("disconnect", () => { terminal.write("\r\n*** Disconnected from backend ***\r\n"); }); @@ -98,7 +100,7 @@ export function NewTerminal({ hostConfig }) { window.removeEventListener("resize", resizeTerminal); socket.disconnect(); }; - }, [hostConfig]); // Re-run effect when hostConfig changes + }, [hostConfig]); return (
{ console.log("New socket connection established"); + let stream = null; let currentCols = 80; let currentRows = 24; - let stream = null; - - socket.on("resize", ({ cols, rows }) => { - console.log(`Terminal resized: cols=${cols}, rows=${rows}`); - currentCols = cols; - currentRows = rows; - if (stream && stream.setWindow) { - stream.setWindow(rows, cols, rows * 100, cols * 100); - console.log(`SSH terminal resized to: cols=${cols}, rows=${rows}`); - } - }); socket.on("connectToHost", (cols, rows, hostConfig) => { if (!hostConfig || !hostConfig.ip || !hostConfig.user || !hostConfig.password || !hostConfig.port) { @@ -36,19 +26,13 @@ io.on("connection", (socket) => { console.log("Received hostConfig:", hostConfig); const { ip, port, user, password } = hostConfig; - if (!ip || !port || !user || !password) { - socket.emit("data", "\r\n*** Missing required connection data ***\r\n"); - return; - } - - console.log("Preparing to connect to host:", hostConfig); const conn = new SSHClient(); - conn .on("ready", function () { console.log("SSH connection established"); socket.emit("data", "\r\n*** SSH CONNECTION ESTABLISHED ***\r\n"); - conn.shell(function (err, newStream) { + + conn.shell({ term: "xterm-256color" }, function (err, newStream) { if (err) { console.error("Error opening SSH shell:", err); return socket.emit( @@ -58,20 +42,35 @@ io.on("connection", (socket) => { } stream = newStream; - stream.setWindow(currentRows, currentCols, currentRows * 100, currentCols * 100); + // Set initial terminal size + stream.setWindow(rows, cols, rows * 100, cols * 100); + console.log(`Initial terminal size: cols=${cols}, rows=${rows}`); + // Pipe SSH output to client + stream.on("data", function (data) { + socket.emit("data", data.toString("binary")); + }); + + stream.on("close", function () { + console.log("SSH stream closed"); + conn.end(); + }); + + // Send keystrokes from terminal to SSH socket.on("data", function (data) { stream.write(data); }); - stream - .on("data", function (d) { - socket.emit("data", d.toString("binary")); - }) - .on("close", function () { - console.log("SSH stream closed"); - conn.end(); - }); + // Resize SSH terminal when client resizes + socket.on("resize", ({ cols, rows }) => { + if (stream && stream.setWindow) { + stream.setWindow(rows, cols, rows * 100, cols * 100); + console.log(`Terminal resized: cols=${cols}, rows=${rows}`); + } + }); + + // Auto-send initial terminal size to backend + socket.emit("resize", { cols, rows }); }); }) .on("close", function () {