* Guacd, Docker-Compose, RDP (#475)

* fix select edit host but not update view (#438)

* fix: Checksum issue with chocolatey

* fix: Remove homebrew old stuff

* Add Korean translation (#439)

Co-authored-by: 송준우 <2484@coreit.co.kr>

* feat: Automate flatpak

* fix: Add imagemagik to electron builder to resolve build error

* fix: Build error with runtime repo flag

* fix: Flatpak runtime error and install freedesktop ver warning

* fix: Flatpak runtime error and install freedesktop ver warning

* feat: Re-add homebrew cask and move scripts to backend

* fix: No sandbox flag issue

* fix: Change name for electron macos cask output

* fix: Sandbox error with Linux

* fix: Remove comming soon for app stores in readme

* Adding Comment at the end of the public_key on the host on deploy (#440)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* -Add New Interface for Credential DB
-Add Credential Name as a comment into the server authorized_key file

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Sudo auto fill password (#441)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Feature Sudo password auto-fill;

* Fix locale json shema;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Added Italian Language; (#445)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Auto collapse snippet folders (#448)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* feat: Add collapsable snippets (customizable in user profile)

* Translations (#447)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

* Fix translations;

Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations.

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Remove PTY-level keepalive (#449)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* feat: add Guacamole support for RDP, VNC, and Telnet connections

- Implemented WebSocket support for Guacamole in Nginx configuration.
- Added REST API endpoints for generating connection tokens and checking guacd status.
- Created Guacamole server using guacamole-lite for handling connections.
- Developed frontend components for testing RDP/VNC connections and displaying the remote session.
- Updated package dependencies to include guacamole-common-js and guacamole-lite.
- Enhanced logging for Guacamole operations.

* feat: enhance Guacamole support with RDP and VNC connection settings and UI updates

* feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation

* fix: finalize adding docker to db

* fix: merge syntax errors

* feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay

* feat: add TypeScript definitions for guacamole-common-js module

* feat: enhance Mouse.State constructor to accept optional parameters and object destructuring

* feat: Add support for RDP and VNC connections in SSH host management

- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.

* feat: Enhance Guacamole integration with extended configuration options

- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings.
- Implemented logging for token requests and received options for better debugging.
- Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values.
- Integrated Guacamole configuration parsing in HostManagerViewer and Host components.
- Enhanced API requests to include extended Guacamole configuration parameters in the token request.
- Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.

* feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes

---------

Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: rename api routes and files

* feat: improve guacd ui/backend

* feat: improve guacd ui/backend

* fix: state persistance issues causing refresh

* feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration

* feat: continue improving integration also with bug fixes

* Merge 2.0.0 with 2.0.0 that includes bug fixes (#620)

* Guacd, Docker-Compose, RDP (#475)

* fix select edit host but not update view (#438)

* fix: Checksum issue with chocolatey

* fix: Remove homebrew old stuff

* Add Korean translation (#439)

Co-authored-by: 송준우 <2484@coreit.co.kr>

* feat: Automate flatpak

* fix: Add imagemagik to electron builder to resolve build error

* fix: Build error with runtime repo flag

* fix: Flatpak runtime error and install freedesktop ver warning

* fix: Flatpak runtime error and install freedesktop ver warning

* feat: Re-add homebrew cask and move scripts to backend

* fix: No sandbox flag issue

* fix: Change name for electron macos cask output

* fix: Sandbox error with Linux

* fix: Remove comming soon for app stores in readme

* Adding Comment at the end of the public_key on the host on deploy (#440)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* -Add New Interface for Credential DB
-Add Credential Name as a comment into the server authorized_key file

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Sudo auto fill password (#441)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Feature Sudo password auto-fill;

* Fix locale json shema;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Added Italian Language; (#445)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Auto collapse snippet folders (#448)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* feat: Add collapsable snippets (customizable in user profile)

* Translations (#447)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

* Fix translations;

Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations.

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Remove PTY-level keepalive (#449)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* feat: add Guacamole support for RDP, VNC, and Telnet connections

- Implemented WebSocket support for Guacamole in Nginx configuration.
- Added REST API endpoints for generating connection tokens and checking guacd status.
- Created Guacamole server using guacamole-lite for handling connections.
- Developed frontend components for testing RDP/VNC connections and displaying the remote session.
- Updated package dependencies to include guacamole-common-js and guacamole-lite.
- Enhanced logging for Guacamole operations.

* feat: enhance Guacamole support with RDP and VNC connection settings and UI updates

* feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation

* fix: finalize adding docker to db

* fix: merge syntax errors

* feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay

* feat: add TypeScript definitions for guacamole-common-js module

* feat: enhance Mouse.State constructor to accept optional parameters and object destructuring

* feat: Add support for RDP and VNC connections in SSH host management

- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.

* feat: Enhance Guacamole integration with extended configuration options

- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings.
- Implemented logging for token requests and received options for better debugging.
- Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values.
- Integrated Guacamole configuration parsing in HostManagerViewer and Host components.
- Enhanced API requests to include extended Guacamole configuration parameters in the token request.
- Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.

* feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes

---------

Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: rename api routes and files

* feat: improve guacd ui/backend

* feat: improve guacd ui/backend

* fix: state persistance issues causing refresh

* feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration

* feat: continue improving integration also with bug fixes

---------

Co-authored-by: Wesley Reid <starhound@lostsouls.org>
Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: allow customizing guacd backened url

* fix: ssh route mistmatching and guacamole url not changing

* chore: increment ver

* feat: change default to work with default compose, added splits creen support, updated readmes

* fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors

* fix: global settings not setting

* chore: update compose

* feat: improve the global status input

* chore: cleanup files

* chore: update export/improt with new host fields

* fix: file manager and docker not loading properly

---------

Co-authored-by: Wesley Reid <starhound@lostsouls.org>
Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Luke Gustafson
2026-03-14 20:05:05 -05:00
committed by GitHub
co-authored by Wesley Reid Tran Trung Kien junu 송준우 SlimGary Nunzio Marfè Claude Sonnet 4.5
parent a255a08903
commit e9e30cd318
85 changed files with 7053 additions and 2286 deletions
+34
View File
@@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState, useMemo } from "react";
import { Terminal } from "@/ui/desktop/apps/features/terminal/Terminal.tsx";
import { ServerStats as ServerView } from "@/ui/desktop/apps/features/server-stats/ServerStats.tsx";
import { FileManager } from "@/ui/desktop/apps/features/file-manager/FileManager.tsx";
import {
GuacamoleDisplay,
type GuacamoleConnectionConfig,
} from "@/ui/desktop/apps/features/guacamole/GuacamoleDisplay.tsx";
import { TunnelManager } from "@/ui/desktop/apps/features/tunnel/TunnelManager.tsx";
import { DockerManager } from "@/ui/desktop/apps/features/docker/DockerManager.tsx";
import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard";
@@ -14,6 +18,7 @@ import {
import * as ResizablePrimitive from "react-resizable-panels";
import { useSidebar } from "@/components/ui/sidebar.tsx";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button.tsx";
import {
TERMINAL_THEMES,
@@ -34,6 +39,7 @@ interface TabData {
};
};
hostConfig?: any;
connectionConfig?: GuacamoleConnectionConfig;
[key: string]: unknown;
}
@@ -105,6 +111,9 @@ export function AppView({
tab.type === "terminal" ||
tab.type === "server_stats" ||
tab.type === "file_manager" ||
tab.type === "rdp" ||
tab.type === "vnc" ||
tab.type === "telnet" ||
tab.type === "tunnel" ||
tab.type === "docker" ||
tab.type === "network_graph",
@@ -376,6 +385,7 @@ export function AppView({
>
{t.type === "terminal" ? (
<Terminal
key={`term-${t.id}-${t.instanceId || ""}`}
ref={t.terminalRef}
hostConfig={t.hostConfig}
isVisible={effectiveVisible}
@@ -387,14 +397,35 @@ export function AppView({
/>
) : t.type === "server_stats" ? (
<ServerView
key={`stats-${t.id}-${t.instanceId || ""}`}
hostConfig={t.hostConfig}
title={t.title}
isVisible={effectiveVisible}
isTopbarOpen={isTopbarOpen}
embedded
/>
) : t.type === "rdp" ||
t.type === "vnc" ||
t.type === "telnet" ? (
t.connectionConfig ? (
<GuacamoleDisplay
key={`guac-${t.id}-${t.instanceId || ""}`}
connectionConfig={t.connectionConfig}
isVisible={effectiveVisible}
onDisconnect={() => removeTab(t.id)}
onError={(err) => {
toast.error(err);
removeTab(t.id);
}}
/>
) : (
<div className="flex items-center justify-center h-full text-red-500">
Missing connection configuration
</div>
)
) : t.type === "network_graph" ? (
<NetworkGraphCard
key={`netgraph-${t.id}-${t.instanceId || ""}`}
isTopbarOpen={isTopbarOpen}
rightSidebarOpen={rightSidebarOpen}
rightSidebarWidth={rightSidebarWidth}
@@ -402,6 +433,7 @@ export function AppView({
/>
) : t.type === "tunnel" ? (
<TunnelManager
key={`tunnel-${t.id}-${t.instanceId || ""}`}
hostConfig={t.hostConfig}
title={t.title}
isVisible={effectiveVisible}
@@ -410,6 +442,7 @@ export function AppView({
/>
) : t.type === "docker" ? (
<DockerManager
key={`docker-${t.id}-${t.instanceId || ""}`}
hostConfig={t.hostConfig}
title={t.title}
isVisible={effectiveVisible}
@@ -419,6 +452,7 @@ export function AppView({
/>
) : (
<FileManager
key={`filemgr-${t.id}-${t.instanceId || ""}`}
embedded
initialHost={t.hostConfig}
onClose={() => removeTab(t.id)}
+21 -40
View File
@@ -36,30 +36,7 @@ import { Button } from "@/components/ui/button.tsx";
import { FolderCard } from "@/ui/desktop/navigation/hosts/FolderCard.tsx";
import { getSSHHosts, getSSHFolders } from "@/ui/main-axios.ts";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
import type { SSHFolder } from "@/types/index.ts";
interface SSHHost {
id: number;
name: string;
ip: string;
port: number;
username: string;
folder: string;
tags: string[];
pin: boolean;
authType: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
enableFileManager: boolean;
defaultPath: string;
tunnelConnections: unknown[];
createdAt: string;
updatedAt: string;
}
import type { SSHFolder, SSHHost } from "@/types/index.ts";
interface SidebarProps {
disabled?: boolean;
@@ -218,37 +195,41 @@ export function LeftSidebar({
setTimeout(() => {
setHosts(newHosts);
prevHostsRef.current = newHosts;
newHosts.forEach((newHost) => {
updateHostConfig(newHost.id, newHost);
});
}, 50);
}
} catch {
setHostsError(t("leftSidebar.failedToLoadHosts"));
}
}, [updateHostConfig]);
}, [t]);
const fetchHostsRef = React.useRef(fetchHosts);
const fetchFolderMetadataRef = React.useRef(fetchFolderMetadata);
React.useEffect(() => {
fetchHosts();
fetchFolderMetadata();
fetchHostsRef.current = fetchHosts;
fetchFolderMetadataRef.current = fetchFolderMetadata;
});
React.useEffect(() => {
fetchHostsRef.current();
fetchFolderMetadataRef.current();
const interval = setInterval(() => {
fetchHosts();
fetchFolderMetadata();
fetchHostsRef.current();
fetchFolderMetadataRef.current();
}, 300000);
return () => clearInterval(interval);
}, [fetchHosts, fetchFolderMetadata]);
}, []);
React.useEffect(() => {
const handleHostsChanged = () => {
fetchHosts();
fetchFolderMetadata();
fetchHostsRef.current();
fetchFolderMetadataRef.current();
};
const handleCredentialsChanged = () => {
fetchHosts();
fetchHostsRef.current();
};
const handleFoldersChanged = () => {
fetchFolderMetadata();
fetchFolderMetadataRef.current();
};
window.addEventListener(
"ssh-hosts:changed",
@@ -276,7 +257,7 @@ export function LeftSidebar({
handleFoldersChanged as EventListener,
);
};
}, [fetchHosts, fetchFolderMetadata]);
}, []);
React.useEffect(() => {
const handler = setTimeout(() => setDebouncedSearch(search), 200);
@@ -549,7 +530,7 @@ export function LeftSidebar({
const metadata = folderMetadata.get(folder);
return (
<FolderCard
key={`folder-${folder}-${hostsByFolder[folder]?.length || 0}`}
key={`folder-${folder}`}
folderName={folder}
hosts={getSortedHosts(hostsByFolder[folder])}
isFirst={idx === 0}
+6
View File
@@ -376,6 +376,8 @@ export function TopNavbar({
const isSshManager = tab.type === "ssh_manager";
const isAdmin = tab.type === "admin";
const isUserProfile = tab.type === "user_profile";
const isRdp = tab.type === "rdp";
const isVnc = tab.type === "vnc";
const isSplittable =
isTerminal || isServer || isFileManager || isTunnel || isDocker;
const disableSplit = !isSplittable;
@@ -495,6 +497,8 @@ export function TopNavbar({
isSshManager ||
isAdmin ||
isUserProfile ||
isRdp ||
isVnc ||
tab.type === "network_graph"
? () => handleTabClose(tab.id)
: undefined
@@ -512,6 +516,8 @@ export function TopNavbar({
isSshManager ||
isAdmin ||
isUserProfile ||
isRdp ||
isVnc ||
tab.type === "network_graph"
}
disableActivate={disableActivate}
+145 -55
View File
@@ -5,6 +5,9 @@ import { ButtonGroup } from "@/components/ui/button-group";
import {
EllipsisVertical,
Terminal,
Monitor,
Eye,
MessagesSquare,
Server,
FolderOpen,
Pencil,
@@ -18,7 +21,7 @@ import {
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
import { getSSHHosts } from "@/ui/main-axios";
import { getSSHHosts, getGuacamoleToken, logActivity } from "@/ui/main-axios";
import type { HostProps } from "../../../../types";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
import { useTranslation } from "react-i18next";
@@ -44,10 +47,16 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
setHost(initialHost);
}, [initialHost]);
useEffect(() => {
const hostIdRef = React.useRef(host.id);
React.useEffect(() => {
hostIdRef.current = host.id;
});
React.useEffect(() => {
const handleHostsChanged = async () => {
const hosts = await getSSHHosts();
const updatedHost = hosts.find((h) => h.id === host.id);
const updatedHost = hosts.find((h) => h.id === hostIdRef.current);
if (updatedHost) {
setHost(updatedHost);
}
@@ -56,7 +65,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
window.addEventListener("ssh-hosts:changed", handleHostsChanged);
return () =>
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
}, [host.id]);
}, []);
useEffect(() => {
const handleShowTagsChanged = () => {
@@ -70,16 +79,21 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
}, []);
const statsConfig = useMemo(() => {
if (!host.statsConfig) {
return DEFAULT_STATS_CONFIG;
}
if (typeof host.statsConfig === "object") {
return host.statsConfig;
}
try {
return host.statsConfig
? JSON.parse(host.statsConfig)
: DEFAULT_STATS_CONFIG;
} catch {
return JSON.parse(host.statsConfig);
} catch (e) {
return DEFAULT_STATS_CONFIG;
}
}, [host.statsConfig]);
const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
const shouldShowStatus = ![false, "false"].includes(
statsConfig.statusCheckEnabled,
);
const shouldShowMetrics = statsConfig.metricsEnabled !== false;
const serverStatus = useHostStatus(host.id, shouldShowStatus);
@@ -96,18 +110,67 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
}
}, [host.tunnelConnections]);
const handleTerminalClick = () => {
const handleTerminalClick = async () => {
if (
host.connectionType === "rdp" ||
host.connectionType === "vnc" ||
host.connectionType === "telnet"
) {
try {
const protocol = host.connectionType as "rdp" | "vnc" | "telnet";
const result = await getGuacamoleToken({
protocol,
hostname: host.ip,
port: host.port,
username: host.username,
password: host.password,
domain: host.domain,
security: host.security,
ignoreCert: host.ignoreCert,
guacamoleConfig: host.guacamoleConfig as any,
});
addTab({
type: protocol,
title,
hostConfig: host,
connectionConfig: {
token: result.token,
protocol,
type: protocol,
hostname: host.ip,
port: host.port,
username: host.username,
password: host.password,
domain: host.domain,
security: host.security,
"ignore-cert": host.ignoreCert,
},
});
try {
await logActivity(protocol, host.id, title);
} catch (err) {
console.warn(`Failed to log ${protocol} activity:`, err);
}
} catch (err) {
console.error("Failed to get Guacamole token:", err);
}
return;
}
addTab({ type: "terminal", title, hostConfig: host });
};
const isSSH = !host.connectionType || host.connectionType === "ssh";
const visibleButtons = [
host.enableTerminal && (host.showTerminalInSidebar ?? true),
host.enableFileManager && (host.showFileManagerInSidebar ?? false),
host.enableTunnel &&
isSSH && host.enableFileManager && (host.showFileManagerInSidebar ?? false),
isSSH &&
host.enableTunnel &&
hasTunnelConnections &&
(host.showTunnelInSidebar ?? false),
host.enableDocker && (host.showDockerInSidebar ?? false),
shouldShowMetrics && (host.showServerStatsInSidebar ?? false),
isSSH && host.enableDocker && (host.showDockerInSidebar ?? false),
isSSH && shouldShowMetrics && (host.showServerStatsInSidebar ?? false),
].filter(Boolean).length;
return (
@@ -133,11 +196,20 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
className="!px-2 border-1 border-edge"
onClick={handleTerminalClick}
>
<Terminal />
{host.connectionType === "rdp" ? (
<Monitor />
) : host.connectionType === "vnc" ? (
<Eye />
) : host.connectionType === "telnet" ? (
<MessagesSquare />
) : (
<Terminal />
)}
</Button>
)}
{host.enableFileManager &&
{isSSH &&
host.enableFileManager &&
(host.showFileManagerInSidebar ?? false) && (
<Button
variant="outline"
@@ -150,7 +222,8 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
</Button>
)}
{host.enableTunnel &&
{isSSH &&
host.enableTunnel &&
hasTunnelConnections &&
(host.showTunnelInSidebar ?? false) && (
<Button
@@ -164,29 +237,33 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
</Button>
)}
{host.enableDocker && (host.showDockerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
>
<Container />
</Button>
)}
{isSSH &&
host.enableDocker &&
(host.showDockerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
>
<Container />
</Button>
)}
{shouldShowMetrics && (host.showServerStatsInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "server_stats", title, hostConfig: host })
}
>
<Server />
</Button>
)}
{isSSH &&
shouldShowMetrics &&
(host.showServerStatsInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "server_stats", title, hostConfig: host })
}
>
<Server />
</Button>
)}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
@@ -211,11 +288,20 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
onClick={handleTerminalClick}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
>
<Terminal className="h-4 w-4" />
{host.connectionType === "rdp" ? (
<Monitor className="h-4 w-4" />
) : host.connectionType === "vnc" ? (
<Eye className="h-4 w-4" />
) : host.connectionType === "telnet" ? (
<MessagesSquare className="h-4 w-4" />
) : (
<Terminal className="h-4 w-4" />
)}
<span className="flex-1">{t("hosts.openTerminal")}</span>
</DropdownMenuItem>
)}
{shouldShowMetrics &&
{isSSH &&
shouldShowMetrics &&
!(host.showServerStatsInSidebar ?? false) && (
<DropdownMenuItem
onClick={() =>
@@ -227,7 +313,8 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
<span className="flex-1">{t("hosts.openServerStats")}</span>
</DropdownMenuItem>
)}
{host.enableFileManager &&
{isSSH &&
host.enableFileManager &&
!(host.showFileManagerInSidebar ?? false) && (
<DropdownMenuItem
onClick={() =>
@@ -239,7 +326,8 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
<span className="flex-1">{t("hosts.openFileManager")}</span>
</DropdownMenuItem>
)}
{host.enableTunnel &&
{isSSH &&
host.enableTunnel &&
hasTunnelConnections &&
!(host.showTunnelInSidebar ?? false) && (
<DropdownMenuItem
@@ -252,17 +340,19 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
<span className="flex-1">{t("hosts.openTunnels")}</span>
</DropdownMenuItem>
)}
{host.enableDocker && !(host.showDockerInSidebar ?? false) && (
<DropdownMenuItem
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
>
<Container className="h-4 w-4" />
<span className="flex-1">{t("hosts.openDocker")}</span>
</DropdownMenuItem>
)}
{isSSH &&
host.enableDocker &&
!(host.showDockerInSidebar ?? false) && (
<DropdownMenuItem
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
>
<Container className="h-4 w-4" />
<span className="flex-1">{t("hosts.openDocker")}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() =>
addTab({
+15 -2
View File
@@ -10,6 +10,9 @@ import {
Server as ServerIcon,
Folder as FolderIcon,
User as UserIcon,
Monitor as MonitorIcon,
Eye as EyeIcon,
MessagesSquare as MessageSquareIcon,
Network,
ArrowDownUp as TunnelIcon,
Container as DockerIcon,
@@ -172,6 +175,9 @@ export function Tab({
tabType === "terminal" ||
tabType === "server_stats" ||
tabType === "file_manager" ||
tabType === "rdp" ||
tabType === "vnc" ||
tabType === "telnet" ||
tabType === "tunnel" ||
tabType === "docker" ||
tabType === "user_profile"
@@ -181,7 +187,6 @@ export function Tab({
const isTunnel = tabType === "tunnel";
const isDocker = tabType === "docker";
const isUserProfile = tabType === "user_profile";
const displayTitle =
title ||
(isServer
@@ -194,7 +199,9 @@ export function Tab({
? t("nav.docker")
: isUserProfile
? t("nav.userProfile")
: t("nav.terminal"));
: tabType === "rdp" || tabType === "vnc" || tabType === "telnet"
? tabType.toUpperCase()
: t("nav.terminal"));
const { base, suffix } = splitTitle(displayTitle);
@@ -219,6 +226,12 @@ export function Tab({
<DockerIcon className="h-4 w-4 flex-shrink-0" />
) : isUserProfile ? (
<UserIcon className="h-4 w-4 flex-shrink-0" />
) : tabType === "rdp" ? (
<MonitorIcon className="h-4 w-4 flex-shrink-0" />
) : tabType === "vnc" ? (
<EyeIcon className="h-4 w-4 flex-shrink-0" />
) : tabType === "telnet" ? (
<MessageSquareIcon className="h-4 w-4 flex-shrink-0" />
) : (
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
)}
+80 -66
View File
@@ -4,6 +4,8 @@ import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
type ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
@@ -51,7 +53,6 @@ interface TabProviderProps {
export function clearTermixSessionStorage() {
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
// Clear all session keys
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
@@ -65,7 +66,6 @@ export function clearTermixSessionStorage() {
export function TabProvider({ children }: TabProviderProps) {
const { t } = useTranslation();
const [tabs, setTabs] = useState<Tab[]>(() => {
// Check if persistence is enabled (always enabled for mobile/Electron)
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as any).electronAPI;
@@ -85,10 +85,9 @@ export function TabProvider({ children }: TabProviderProps) {
let maxId = 1;
for (const tab of parsed) {
if (tab.type === "home") continue;
// Preserve instanceId from saved tab - critical for session lookup
const restoredTab: Tab = {
...tab,
instanceId: tab.instanceId, // Preserve instanceId for session persistence
instanceId: tab.instanceId,
terminalRef:
tab.type === "terminal"
? React.createRef<{ disconnect?: () => void }>()
@@ -115,7 +114,6 @@ export function TabProvider({ children }: TabProviderProps) {
const saved = localStorage.getItem("termix_currentTab");
if (saved) {
const parsed = parseInt(saved, 10);
// Validate against restored tabs
if (parsed && tabs.some((t) => t.id === parsed)) return parsed;
}
} catch {
@@ -133,9 +131,7 @@ export function TabProvider({ children }: TabProviderProps) {
});
const nextTabId = useRef(initialMaxId);
// Save tabs to localStorage on change
useEffect(() => {
// Check if persistence is enabled (always enabled for mobile/Electron)
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as any).electronAPI;
@@ -144,14 +140,12 @@ export function TabProvider({ children }: TabProviderProps) {
const shouldSave = isMobile || isElectron || persistenceEnabled;
if (shouldSave) {
// Serialize tabs, preserving instanceId for session persistence
const serializable = tabs
.filter((t) => t.type !== "home")
.map(({ terminalRef, ...rest }) => rest);
localStorage.setItem("termix_tabs", JSON.stringify(serializable));
localStorage.setItem("termix_currentTab", String(currentTab));
} else {
// Clear saved tabs when persistence is disabled
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
}
@@ -346,72 +340,92 @@ export function TabProvider({ children }: TabProviderProps) {
});
};
const updateHostConfig = (
hostId: number,
newHostConfig: {
id: number;
name?: string;
username: string;
ip: string;
port: number;
},
) => {
setTabs((prev) =>
prev.map((tab) => {
if (tab.hostConfig && tab.hostConfig.id === hostId) {
if (tab.type === "ssh_manager") {
const updateHostConfig = useCallback(
(
hostId: number,
newHostConfig: {
id: number;
name?: string;
username: string;
ip: string;
port: number;
},
) => {
setTabs((prev) =>
prev.map((tab) => {
if (tab.hostConfig && tab.hostConfig.id === hostId) {
if (tab.type === "ssh_manager") {
return {
...tab,
hostConfig: {
...newHostConfig,
instanceId: tab.hostConfig.instanceId,
},
};
}
return {
...tab,
hostConfig: {
...newHostConfig,
instanceId: tab.hostConfig.instanceId, // Preserve instanceId for session persistence
instanceId: tab.hostConfig.instanceId,
},
title: newHostConfig.name?.trim()
? newHostConfig.name
: t("nav.hostTabTitle", {
username: newHostConfig.username,
ip: newHostConfig.ip,
port: newHostConfig.port,
}),
};
}
return tab;
}),
);
},
[t],
);
return {
...tab,
hostConfig: {
...newHostConfig,
instanceId: tab.hostConfig.instanceId, // Preserve instanceId for session persistence
},
title: newHostConfig.name?.trim()
? newHostConfig.name
: t("nav.hostTabTitle", {
username: newHostConfig.username,
ip: newHostConfig.ip,
port: newHostConfig.port,
}),
};
}
return tab;
}),
);
};
const updateTab = useCallback(
(tabId: number, updates: Partial<Omit<Tab, "id">>) => {
setTabs((prev) =>
prev.map((tab) =>
tab.id === tabId
? { ...tab, ...updates, _updateTimestamp: Date.now() }
: tab,
),
);
},
[],
);
const updateTab = (tabId: number, updates: Partial<Omit<Tab, "id">>) => {
setTabs((prev) =>
prev.map((tab) =>
tab.id === tabId
? { ...tab, ...updates, _updateTimestamp: Date.now() }
: tab,
),
);
};
const value: TabContextType = {
tabs,
currentTab,
allSplitScreenTab,
addTab,
removeTab,
setCurrentTab,
setSplitScreenTab,
getTab,
reorderTabs,
updateHostConfig,
updateTab,
};
const value: TabContextType = useMemo(
() => ({
tabs,
currentTab,
allSplitScreenTab,
addTab,
removeTab,
setCurrentTab,
setSplitScreenTab,
getTab,
reorderTabs,
updateHostConfig,
updateTab,
}),
[
tabs,
currentTab,
allSplitScreenTab,
addTab,
removeTab,
setSplitScreenTab,
getTab,
reorderTabs,
updateHostConfig,
updateTab,
],
);
return <TabContext.Provider value={value}>{children}</TabContext.Provider>;
}