* 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
@@ -0,0 +1,12 @@
import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager.tsx";
import React from "react";
const HostManagerApp: React.FC = () => {
return (
<div className="w-full h-screen">
<HostManager isTopbarOpen={false} onSelectView={() => {}} />
</div>
);
};
export default HostManagerApp;
@@ -53,6 +53,7 @@ import {
revokeHostAccess,
getSSHHostById,
notifyHostCreatedOrUpdated,
getGuacamoleSettings,
type Role,
type AccessRecord,
} from "@/ui/main-axios.ts";
@@ -130,7 +131,9 @@ import { HostDockerTab } from "./tabs/HostDockerTab";
import { HostTunnelTab } from "./tabs/HostTunnelTab";
import { HostFileManagerTab } from "./tabs/HostFileManagerTab";
import { HostStatisticsTab } from "./tabs/HostStatisticsTab";
import { HostStatusTab } from "./tabs/HostStatusTab";
import { HostSharingTab } from "./tabs/HostSharingTab";
import { HostRemoteDesktopTab } from "./tabs/HostRemoteDesktopTab";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
interface User {
@@ -176,11 +179,18 @@ export function HostManagerEditor({
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState("general");
const [formError, setFormError] = useState<string | null>(null);
const [guacEnabled, setGuacEnabled] = useState(true);
useEffect(() => {
setFormError(null);
}, [activeTab]);
useEffect(() => {
getGuacamoleSettings()
.then((data) => setGuacEnabled(data.enabled))
.catch(() => {});
}, []);
const [statusIntervalUnit, setStatusIntervalUnit] = useState<
"seconds" | "minutes"
>("seconds");
@@ -265,10 +275,11 @@ export function HostManagerEditor({
const formSchema = z
.object({
connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"),
name: z.string().optional(),
ip: z.string().min(1),
ip: z.string().min(1, t("hosts.ipRequired", "IP address is required")),
port: z.coerce.number().min(1).max(65535),
username: z.string().min(1),
username: z.string().optional(),
folder: z.string().optional(),
tags: z.array(z.string().min(1)).default([]),
pin: z.boolean().default(false),
@@ -350,6 +361,7 @@ export function HostManagerEditor({
metricsEnabled: z.boolean().default(true),
metricsInterval: z.number().min(5).max(3600).default(30),
useGlobalMetricsInterval: z.boolean().default(true),
disableTcpPing: z.boolean().default(false),
})
.default({
enabledWidgets: [
@@ -369,6 +381,7 @@ export function HostManagerEditor({
metricsEnabled: true,
metricsInterval: 30,
useGlobalMetricsInterval: true,
disableTcpPing: false,
}),
terminalConfig: z
.object({
@@ -436,6 +449,10 @@ export function HostManagerEditor({
)
.optional(),
enableDocker: z.boolean().default(false),
domain: z.string().optional(),
security: z.string().optional(),
ignoreCert: z.boolean().default(true),
guacamoleConfig: z.record(z.string(), z.unknown()).optional(),
showTerminalInSidebar: z.boolean().default(true),
showFileManagerInSidebar: z.boolean().default(false),
showTunnelInSidebar: z.boolean().default(false),
@@ -443,6 +460,20 @@ export function HostManagerEditor({
showServerStatsInSidebar: z.boolean().default(false),
})
.superRefine((data, ctx) => {
if (data.connectionType !== "ssh") {
return;
}
if (!data.username || data.username.trim() === "") {
if (data.authType !== "credential") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.usernameRequired", "Username is required"),
path: ["username"],
});
}
}
if (data.authType === "none") {
return;
}
@@ -458,25 +489,27 @@ export function HostManagerEditor({
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.passwordRequired"),
message: t("hosts.passwordRequired", "Password is required"),
path: ["password"],
});
}
} else if (data.authType === "key") {
if (
!data.key ||
(typeof data.key === "string" && data.key.trim() === "")
(typeof data.key === "string" &&
data.key.trim() === "" &&
data.key !== "existing_key")
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.sshKeyRequired"),
message: t("hosts.sshKeyRequired", "SSH key is required"),
path: ["key"],
});
}
if (!data.keyType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.keyTypeRequired"),
message: t("hosts.keyTypeRequired", "Key type is required"),
path: ["keyType"],
});
}
@@ -484,7 +517,7 @@ export function HostManagerEditor({
if (!data.credentialId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.credentialRequired"),
message: t("hosts.credentialRequired", "Credential is required"),
path: ["credentialId"],
});
}
@@ -510,6 +543,7 @@ export function HostManagerEditor({
resolver: zodResolver(formSchema) as any,
mode: "all",
defaultValues: {
connectionType: "ssh" as const,
name: "",
ip: "",
port: 22,
@@ -547,32 +581,80 @@ export function HostManagerEditor({
socks5Password: "",
socks5ProxyChain: [],
enableDocker: false,
domain: "",
security: "any",
ignoreCert: true,
guacamoleConfig: {},
},
});
const watchedFields = form.watch();
const formState = form.formState;
const watchedConnectionType = form.watch("connectionType") || "ssh";
const prevConnectionTypeRef = useRef<string>("ssh");
useEffect(() => {
const prev = prevConnectionTypeRef.current;
const current = watchedConnectionType;
if (prev === current) return;
prevConnectionTypeRef.current = current;
const portDefaults: Record<string, number> = {
ssh: 22,
rdp: 3389,
vnc: 5900,
telnet: 23,
};
const currentPort = form.getValues("port");
const oldDefault = portDefaults[prev] || 22;
if (currentPort === oldDefault) {
form.setValue("port", portDefaults[current] || 22);
}
if (current !== "ssh") {
const currentStatsConfig = form.getValues("statsConfig");
form.setValue("statsConfig", {
...currentStatsConfig,
metricsEnabled: false,
disableTcpPing: false,
});
}
if (activeTab !== "general" && current !== "ssh") {
setActiveTab("general");
}
}, [watchedConnectionType]);
const isFormValid = React.useMemo(() => {
const values = form.getValues();
const errors = formState.errors;
if (!values.ip || !values.username || values.username.trim() === "")
if (!watchedFields.ip) return false;
if (watchedFields.connectionType !== "ssh") {
const port = Number(watchedFields.port);
return !errors.ip && port >= 1 && port <= 65535;
}
if (!watchedFields.username || watchedFields.username.trim() === "")
return false;
if (authTab === "password") {
return !!(values.password && values.password.trim() !== "");
if (!watchedFields.password || watchedFields.password.trim() === "")
return false;
} else if (authTab === "key") {
return !!(values.key && values.keyType);
if (!watchedFields.key || !watchedFields.keyType) return false;
} else if (authTab === "credential") {
return !!values.credentialId;
if (!watchedFields.credentialId) return false;
} else if (authTab === "none") {
return true;
// No auth required
} else if (authTab === "opkssh") {
return true;
// No auth required
} else {
return false;
}
return false;
}, [watchedFields, authTab]);
return Object.keys(errors).length === 0;
}, [watchedFields, authTab, formState.errors]);
useEffect(() => {
const updateAuthFields = async () => {
@@ -605,7 +687,10 @@ export function HostManagerEditor({
}
}
} else if (authTab === "none") {
form.setValue("password", "", { shouldValidate: true });
const connectionType = form.getValues("connectionType");
if (connectionType === "ssh") {
form.setValue("password", "", { shouldValidate: true });
}
form.setValue("key", null, { shouldValidate: true });
form.setValue("keyPassword", "", { shouldValidate: true });
form.setValue("keyType", "auto", { shouldValidate: true });
@@ -628,14 +713,16 @@ export function HostManagerEditor({
useEffect(() => {
if (editingHost) {
const cleanedHost = { ...editingHost };
if (cleanedHost.credentialId && cleanedHost.key) {
cleanedHost.key = undefined;
cleanedHost.keyPassword = undefined;
cleanedHost.keyType = undefined;
} else if (cleanedHost.credentialId && cleanedHost.password) {
cleanedHost.password = undefined;
} else if (cleanedHost.key && cleanedHost.password) {
cleanedHost.password = undefined;
if ((cleanedHost as any).connectionType === "ssh") {
if (cleanedHost.credentialId && cleanedHost.key) {
cleanedHost.key = undefined;
cleanedHost.keyPassword = undefined;
cleanedHost.keyType = undefined;
} else if (cleanedHost.credentialId && cleanedHost.password) {
cleanedHost.password = undefined;
} else if (cleanedHost.key && cleanedHost.password) {
cleanedHost.password = undefined;
}
}
const defaultAuthType = (cleanedHost.authType ||
@@ -669,6 +756,7 @@ export function HostManagerEditor({
parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig };
const formData: Partial<FormData> = {
connectionType: (cleanedHost as any).connectionType || "ssh",
name: cleanedHost.name || "",
ip: cleanedHost.ip || "",
port: cleanedHost.port || 22,
@@ -734,6 +822,21 @@ export function HostManagerEditor({
? cleanedHost.socks5ProxyChain
: [],
enableDocker: Boolean(cleanedHost.enableDocker),
domain: (cleanedHost as any).domain || "",
security: (cleanedHost as any).security || "any",
ignoreCert: (cleanedHost as any).ignoreCert ?? true,
guacamoleConfig: (() => {
const cfg = (cleanedHost as any).guacamoleConfig;
if (!cfg) return {};
if (typeof cfg === "string") {
try {
return JSON.parse(cfg);
} catch {
return {};
}
}
return cfg;
})(),
showTerminalInSidebar: cleanedHost.showTerminalInSidebar ?? true,
showFileManagerInSidebar: cleanedHost.showFileManagerInSidebar ?? false,
showTunnelInSidebar: cleanedHost.showTunnelInSidebar ?? false,
@@ -750,7 +853,11 @@ export function HostManagerEditor({
setProxyMode("single");
}
if (defaultAuthType === "password") {
if (cleanedHost.connectionType !== "ssh") {
if (cleanedHost.password) {
formData.password = cleanedHost.password;
}
} else if (defaultAuthType === "password") {
formData.password = cleanedHost.password || "";
} else if (defaultAuthType === "key") {
formData.key = editingHost.id ? "existing_key" : editingHost.key;
@@ -774,6 +881,7 @@ export function HostManagerEditor({
} else {
setAuthTab("password");
const defaultFormData: Partial<FormData> = {
connectionType: "ssh" as const,
name: "",
ip: "",
port: 22,
@@ -799,6 +907,10 @@ export function HostManagerEditor({
terminalConfig: DEFAULT_TERMINAL_CONFIG,
forceKeyboardInteractive: false,
enableDocker: false,
domain: "",
security: "any",
ignoreCert: true,
guacamoleConfig: {},
showTerminalInSidebar: true,
showFileManagerInSidebar: false,
showTunnelInSidebar: false,
@@ -834,23 +946,46 @@ export function HostManagerEditor({
...data,
};
if (
data.terminalConfig?.sudoPasswordAutoFill &&
data.terminalConfig?.sudoPassword
) {
submitData.sudoPassword = data.terminalConfig.sudoPassword;
}
(submitData as any).connectionType = data.connectionType;
(submitData as any).domain = data.domain;
(submitData as any).security = data.security;
(submitData as any).ignoreCert = data.ignoreCert;
(submitData as any).guacamoleConfig = data.guacamoleConfig;
if (data.authType !== "credential") {
submitData.credentialId = undefined;
}
if (data.authType !== "password") {
submitData.password = undefined;
}
if (data.authType !== "key") {
if (data.connectionType !== "ssh") {
submitData.authType = "none";
submitData.key = undefined;
submitData.keyPassword = undefined;
submitData.keyType = undefined;
submitData.credentialId = undefined;
submitData.tunnelConnections = [];
submitData.jumpHosts = [];
(submitData as any).useSocks5 = false;
(submitData as any).socks5ProxyChain = [];
submitData.forceKeyboardInteractive = false;
submitData.enableTunnel = false;
submitData.enableFileManager = false;
submitData.enableDocker = false;
submitData.enableTerminal = true;
} else {
if (
data.terminalConfig?.sudoPasswordAutoFill &&
data.terminalConfig?.sudoPassword
) {
submitData.sudoPassword = data.terminalConfig.sudoPassword;
}
if (data.authType !== "credential") {
submitData.credentialId = undefined;
}
if (data.authType !== "password") {
submitData.password = undefined;
}
if (data.authType !== "key") {
submitData.key = undefined;
submitData.keyPassword = undefined;
submitData.keyType = undefined;
}
}
if (data.authType === "key") {
@@ -955,6 +1090,11 @@ export function HostManagerEditor({
enableTerminal: "terminal",
terminalConfig: "terminal",
enableDocker: "docker",
domain: "general",
security: "general",
ignoreCert: "general",
guacamoleConfig: "remote_desktop",
connectionType: "general",
enableTunnel: "tunnel",
tunnelConnections: "tunnel",
enableFileManager: "file_manager",
@@ -1194,6 +1334,50 @@ export function HostManagerEditor({
: t("hosts.addHost")}
</h3>
</div>
{guacEnabled && (
<FormField
control={form.control}
name="connectionType"
render={({ field }) => (
<FormItem className="mb-4">
<FormControl>
<Tabs
value={field.value || "ssh"}
onValueChange={field.onChange}
className="w-full"
>
<TabsList className="bg-button border border-edge-medium">
<TabsTrigger
value="ssh"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.ssh")}
</TabsTrigger>
<TabsTrigger
value="rdp"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.rdp")}
</TabsTrigger>
<TabsTrigger
value="vnc"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.vnc")}
</TabsTrigger>
<TabsTrigger
value="telnet"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.telnet")}
</TabsTrigger>
</TabsList>
</Tabs>
</FormControl>
</FormItem>
)}
/>
)}
<Tabs
value={activeTab}
onValueChange={setActiveTab}
@@ -1206,45 +1390,63 @@ export function HostManagerEditor({
>
{t("hosts.general")}
</TabsTrigger>
<TabsTrigger
value="terminal"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger
value="docker"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
Docker
</TabsTrigger>
<TabsTrigger
value="tunnel"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.tunnel")}
</TabsTrigger>
<TabsTrigger
value="file_manager"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.fileManager")}
</TabsTrigger>
{watchedConnectionType === "ssh" && (
<>
<TabsTrigger
value="terminal"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger
value="docker"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
Docker
</TabsTrigger>
<TabsTrigger
value="tunnel"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.tunnel")}
</TabsTrigger>
<TabsTrigger
value="file_manager"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.fileManager")}
</TabsTrigger>
</>
)}
<TabsTrigger
value="statistics"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.statistics")}
{watchedConnectionType === "ssh"
? t("hosts.statistics")
: t("hosts.status")}
</TabsTrigger>
{!editingHost?.isShared && (
<TabsTrigger value="sharing">
{t("rbac.sharing")}
{watchedConnectionType !== "ssh" && (
<TabsTrigger
value="remote_desktop"
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
>
{t("hosts.remoteDesktop")}
</TabsTrigger>
)}
{watchedConnectionType === "ssh" &&
!editingHost?.isShared && (
<TabsTrigger value="sharing">
{t("rbac.sharing")}
</TabsTrigger>
)}
</TabsList>
<TabsContent value="general" className="pt-2">
<HostGeneralTab
form={form}
connectionType={
watchedConnectionType as "ssh" | "rdp" | "vnc" | "telnet"
}
authTab={authTab}
setAuthTab={setAuthTab}
keyInputMethod={keyInputMethod}
@@ -1273,44 +1475,70 @@ export function HostManagerEditor({
t={t}
/>
</TabsContent>
<TabsContent value="terminal" className="space-y-1">
<HostTerminalTab form={form} snippets={snippets} t={t} />
</TabsContent>
<TabsContent value="docker" className="space-y-4">
<HostDockerTab form={form} t={t} />
</TabsContent>
<TabsContent value="tunnel">
<HostTunnelTab
form={form}
sshConfigDropdownOpen={sshConfigDropdownOpen}
setSshConfigDropdownOpen={setSshConfigDropdownOpen}
sshConfigInputRefs={sshConfigInputRefs}
sshConfigDropdownRefs={sshConfigDropdownRefs}
getFilteredSshConfigs={getFilteredSshConfigs}
handleSshConfigClick={handleSshConfigClick}
t={t}
/>
</TabsContent>
<TabsContent value="file_manager">
<HostFileManagerTab form={form} t={t} />
</TabsContent>
{watchedConnectionType === "ssh" && (
<>
<TabsContent value="terminal" className="space-y-1">
<HostTerminalTab form={form} snippets={snippets} t={t} />
</TabsContent>
<TabsContent value="docker" className="space-y-4">
<HostDockerTab form={form} t={t} />
</TabsContent>
<TabsContent value="tunnel">
<HostTunnelTab
form={form}
sshConfigDropdownOpen={sshConfigDropdownOpen}
setSshConfigDropdownOpen={setSshConfigDropdownOpen}
sshConfigInputRefs={sshConfigInputRefs}
sshConfigDropdownRefs={sshConfigDropdownRefs}
getFilteredSshConfigs={getFilteredSshConfigs}
handleSshConfigClick={handleSshConfigClick}
t={t}
/>
</TabsContent>
<TabsContent value="file_manager">
<HostFileManagerTab form={form} t={t} />
</TabsContent>
</>
)}
<TabsContent value="statistics" className="space-y-6">
<HostStatisticsTab
form={form}
statusIntervalUnit={statusIntervalUnit}
setStatusIntervalUnit={setStatusIntervalUnit}
metricsIntervalUnit={metricsIntervalUnit}
setMetricsIntervalUnit={setMetricsIntervalUnit}
snippets={snippets}
t={t}
/>
</TabsContent>
<TabsContent value="sharing" className="space-y-6">
<HostSharingTab
hostId={editingHost?.id}
isNewHost={!editingHost}
/>
{watchedConnectionType === "ssh" ? (
<HostStatisticsTab
form={form}
statusIntervalUnit={statusIntervalUnit}
setStatusIntervalUnit={setStatusIntervalUnit}
metricsIntervalUnit={metricsIntervalUnit}
setMetricsIntervalUnit={setMetricsIntervalUnit}
snippets={snippets}
t={t}
/>
) : (
<HostStatusTab
form={form}
statusIntervalUnit={statusIntervalUnit}
setStatusIntervalUnit={setStatusIntervalUnit}
t={t}
/>
)}
</TabsContent>
{watchedConnectionType !== "ssh" && (
<TabsContent value="remote_desktop" className="space-y-4">
<HostRemoteDesktopTab
form={form}
connectionType={
watchedConnectionType as "rdp" | "vnc" | "telnet"
}
t={t}
/>
</TabsContent>
)}
{watchedConnectionType === "ssh" && (
<TabsContent value="sharing" className="space-y-6">
<HostSharingTab
hostId={editingHost?.id}
isNewHost={!editingHost}
/>
</TabsContent>
)}
</Tabs>
</div>
</ScrollArea>
@@ -41,6 +41,9 @@ import {
refreshServerPolling,
isElectron,
getConfiguredServerUrl,
getGuacamoleTokenFromHost,
getGuacamoleToken,
logActivity,
} from "@/ui/main-axios.ts";
import { useServerStatus } from "@/ui/contexts/ServerStatusContext";
import { toast } from "sonner";
@@ -83,6 +86,9 @@ import {
Plus,
ListChecks,
ChevronDown,
Monitor,
MessagesSquare,
Eye,
} from "lucide-react";
import type {
SSHHost,
@@ -587,6 +593,7 @@ export function HostManagerViewer({
const getSampleData = () => ({
hosts: [
{
connectionType: "ssh",
name: t("interface.webServerProduction"),
ip: "192.168.1.100",
port: 22,
@@ -607,6 +614,7 @@ export function HostManagerViewer({
defaultPath: "/var/www",
},
{
connectionType: "ssh",
name: t("interface.databaseServer"),
ip: "192.168.1.101",
port: 22,
@@ -645,6 +653,7 @@ export function HostManagerViewer({
},
},
{
connectionType: "ssh",
name: t("interface.developmentServer"),
ip: "192.168.1.102",
port: 2222,
@@ -667,6 +676,66 @@ export function HostManagerViewer({
sudoPassword: "dev_sudo_password",
},
{
connectionType: "rdp",
name: "Windows Server 2022",
ip: "192.168.1.200",
port: 3389,
username: "Administrator",
password: "windows_password",
domain: "COMPANY",
security: "nla",
ignoreCert: false,
folder: "Remote Desktop",
tags: ["rdp", "windows", "production"],
pin: false,
notes: "Production Windows Server with RDP access",
guacamoleConfig: {
"enable-drive": true,
"drive-path": "/shared",
"create-drive-path": true,
"server-layout": "en-us-qwerty",
"resize-method": "display-update",
},
},
{
connectionType: "vnc",
name: "Ubuntu Desktop",
ip: "192.168.1.201",
port: 5900,
username: "vncuser",
password: "vnc_password",
folder: "Remote Desktop",
tags: ["vnc", "linux", "desktop"],
pin: false,
notes: "Ubuntu desktop with VNC server",
guacamoleConfig: {
"color-depth": 24,
cursor: "remote",
"read-only": false,
"clipboard-encoding": "UTF-8",
},
},
{
connectionType: "telnet",
name: "Network Switch",
ip: "192.168.1.254",
port: 23,
username: "admin",
password: "switch_password",
folder: "Infrastructure",
tags: ["telnet", "network", "switch"],
pin: false,
notes: "Legacy network switch with Telnet access",
guacamoleConfig: {
"color-scheme": "green-black",
"font-name": "monospace",
"font-size": 12,
scrollback: 1024,
backspace: 127,
},
},
{
connectionType: "ssh",
name: "Jump Host Server",
ip: "10.0.0.50",
port: 22,
@@ -693,6 +762,7 @@ export function HostManagerViewer({
],
},
{
connectionType: "ssh",
name: "Server with SOCKS5 Proxy",
ip: "10.10.10.100",
port: 22,
@@ -713,6 +783,7 @@ export function HostManagerViewer({
socks5Password: "proxypass",
},
{
connectionType: "ssh",
name: "Customized Terminal Server",
ip: "192.168.1.150",
port: 22,
@@ -1388,34 +1459,47 @@ export function HostManagerViewer({
)}
{(() => {
const statsConfig = (() => {
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;
}
})();
const shouldShowStatus =
statsConfig.statusCheckEnabled !==
false;
const serverStatus = getStatus(host.id);
const shouldShowStatus = ![
false,
"false",
].includes(
statsConfig.statusCheckEnabled,
);
return shouldShowStatus ? (
if (!shouldShowStatus) return null;
const serverStatus = getStatus(host.id);
return (
<Status
status={serverStatus}
className="!bg-transparent !p-0.75 flex-shrink-0"
>
<StatusIndicator />
</Status>
) : null;
);
})()}
{host.pin && (
<Pin className="h-3 w-3 text-yellow-500 flex-shrink-0" />
)}
<h3 className="font-medium truncate text-sm">
{host.name ||
`${host.username}@${host.ip}`}
(host.username
? `${host.username}@${host.ip}`
: host.ip)}
</h3>
{(host as any).isShared && (
<Badge
@@ -1429,9 +1513,11 @@ export function HostManagerViewer({
<p className="text-xs text-muted-foreground truncate">
{host.ip}:{host.port}
</p>
<p className="text-xs text-muted-foreground truncate">
{host.username}
</p>
{host.username && (
<p className="text-xs text-muted-foreground truncate">
{host.username}
</p>
)}
<p className="text-xs text-muted-foreground truncate">
ID: {host.id}
</p>
@@ -1569,74 +1655,118 @@ export function HostManagerViewer({
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
{host.enableTerminal && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"terminal",
);
}}
>
<Terminal className="h-4 w-4 mr-2" />
{t("hosts.copyTerminalUrl")}
</DropdownMenuItem>
)}
{host.enableFileManager && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"file-manager",
);
}}
>
<FolderOpen className="h-4 w-4 mr-2" />
{t("hosts.copyFileManagerUrl")}
</DropdownMenuItem>
)}
{host.enableTunnel && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"tunnel",
);
}}
>
<ArrowDownUp className="h-4 w-4 mr-2" />
{t("hosts.copyTunnelUrl")}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"server-stats",
{(() => {
const connType = (host as any)
.connectionType;
const isRemoteDesktop =
connType === "rdp" ||
connType === "vnc" ||
connType === "telnet";
if (isRemoteDesktop) {
return (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
connType,
);
}}
>
{connType === "rdp" ? (
<Monitor className="h-4 w-4 mr-2" />
) : connType === "vnc" ? (
<Eye className="h-4 w-4 mr-2" />
) : (
<MessagesSquare className="h-4 w-4 mr-2" />
)}
{t(
"hosts.copyRemoteDesktopUrl",
)}
</DropdownMenuItem>
);
}}
>
<Server className="h-4 w-4 mr-2" />
{t("hosts.copyServerStatsUrl")}
</DropdownMenuItem>
{host.enableDocker && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"docker",
);
}}
>
<Container className="h-4 w-4 mr-2" />
{t("hosts.copyDockerUrl")}
</DropdownMenuItem>
)}
}
return (
<>
{host.enableTerminal && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"terminal",
);
}}
>
<Terminal className="h-4 w-4 mr-2" />
{t(
"hosts.copyTerminalUrl",
)}
</DropdownMenuItem>
)}
{host.enableFileManager && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"file-manager",
);
}}
>
<FolderOpen className="h-4 w-4 mr-2" />
{t(
"hosts.copyFileManagerUrl",
)}
</DropdownMenuItem>
)}
{host.enableTunnel && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"tunnel",
);
}}
>
<ArrowDownUp className="h-4 w-4 mr-2" />
{t("hosts.copyTunnelUrl")}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"server-stats",
);
}}
>
<Server className="h-4 w-4 mr-2" />
{t(
"hosts.copyServerStatsUrl",
)}
</DropdownMenuItem>
{host.enableDocker && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
copyFullScreenUrl(
host,
"docker",
);
}}
>
<Container className="h-4 w-4 mr-2" />
{t("hosts.copyDockerUrl")}
</DropdownMenuItem>
)}
</>
);
})()}
</DropdownMenuContent>
</DropdownMenu>
</>
@@ -1671,185 +1801,339 @@ export function HostManagerViewer({
)}
<div className="flex flex-wrap gap-1">
{host.enableTerminal && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Terminal className="h-2 w-2 mr-0.5" />
{t("hosts.terminalBadge")}
</Badge>
)}
{host.enableTunnel && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Network className="h-2 w-2 mr-0.5" />
{t("hosts.tunnelBadge")}
{host.tunnelConnections &&
host.tunnelConnections.length > 0 && (
<span className="ml-0.5">
({host.tunnelConnections.length})
</span>
{(() => {
const connType = (host as any)
.connectionType;
if (connType === "rdp") {
return (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Monitor className="h-2 w-2 mr-0.5" />
{t("hosts.rdp")}
</Badge>
);
}
if (connType === "vnc") {
return (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Eye className="h-2 w-2 mr-0.5" />
{t("hosts.vnc")}
</Badge>
);
}
if (connType === "telnet") {
return (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<MessagesSquare className="h-2 w-2 mr-0.5" />
{t("hosts.telnet")}
</Badge>
);
}
return (
<>
{host.enableTerminal && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Terminal className="h-2 w-2 mr-0.5" />
{t("hosts.terminalBadge")}
</Badge>
)}
</Badge>
)}
{host.enableFileManager && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<FileEdit className="h-2 w-2 mr-0.5" />
{t("hosts.fileManagerBadge")}
</Badge>
)}
{host.enableDocker && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Container className="h-2 w-2 mr-0.5" />
{t("hosts.docker")}
</Badge>
)}
{host.enableTunnel && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Network className="h-2 w-2 mr-0.5" />
{t("hosts.tunnelBadge")}
{host.tunnelConnections &&
host.tunnelConnections.length >
0 && (
<span className="ml-0.5">
(
{
host.tunnelConnections
.length
}
)
</span>
)}
</Badge>
)}
{host.enableFileManager && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<FileEdit className="h-2 w-2 mr-0.5" />
{t("hosts.fileManagerBadge")}
</Badge>
)}
{host.enableDocker && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Container className="h-2 w-2 mr-0.5" />
{t("hosts.docker")}
</Badge>
)}
</>
);
})()}
</div>
</div>
<div className="mt-3 pt-3 border-t border-border/50 flex items-center justify-center gap-1">
{host.enableTerminal && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "terminal",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1"
>
<Terminal className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openTerminal")}</p>
</TooltipContent>
</Tooltip>
)}
{host.enableFileManager && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "file_manager",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-emerald-500/10 hover:border-emerald-500/50 flex-1"
>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open File Manager</p>
</TooltipContent>
</Tooltip>
)}
{host.enableTunnel && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "tunnel",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-orange-500/10 hover:border-orange-500/50 flex-1"
>
<ArrowDownUp className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Tunnels</p>
</TooltipContent>
</Tooltip>
)}
{host.enableDocker && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "docker",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-cyan-500/10 hover:border-cyan-500/50 flex-1"
>
<Container className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openDocker")}</p>
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "server_stats",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-purple-500/10 hover:border-purple-500/50 flex-1"
>
<Server className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Server Details</p>
</TooltipContent>
</Tooltip>
{(() => {
const connType = (host as any)
.connectionType;
const isRemoteDesktop =
connType === "rdp" ||
connType === "vnc" ||
connType === "telnet";
if (isRemoteDesktop) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={async (e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.ip}:${host.port}`;
try {
const protocol = connType 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 (error) {
toast.error(
`Failed to connect: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}}
className="h-7 px-2 hover:bg-indigo-500/10 hover:border-indigo-500/50 flex-1"
>
{connType === "rdp" ? (
<Monitor className="h-3.5 w-3.5" />
) : connType === "vnc" ? (
<Eye className="h-3.5 w-3.5" />
) : (
<MessagesSquare className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.remoteDesktop")}</p>
</TooltipContent>
</Tooltip>
);
}
return (
<>
{host.enableTerminal && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title =
host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "terminal",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1"
>
<Terminal className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openTerminal")}</p>
</TooltipContent>
</Tooltip>
)}
{host.enableFileManager && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title =
host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "file_manager",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-emerald-500/10 hover:border-emerald-500/50 flex-1"
>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("hosts.openFileManager")}
</p>
</TooltipContent>
</Tooltip>
)}
{host.enableTunnel && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title =
host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "tunnel",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-orange-500/10 hover:border-orange-500/50 flex-1"
>
<ArrowDownUp className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openTunnels")}</p>
</TooltipContent>
</Tooltip>
)}
{host.enableDocker && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title =
host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "docker",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-cyan-500/10 hover:border-cyan-500/50 flex-1"
>
<Container className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openDocker")}</p>
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "server_stats",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-purple-500/10 hover:border-purple-500/50 flex-1"
>
<Server className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("hosts.openServerStats")}</p>
</TooltipContent>
</Tooltip>
</>
);
})()}
</div>
</div>
</TooltipTrigger>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,797 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form.tsx";
import { Input } from "@/components/ui/input.tsx";
import { Switch } from "@/components/ui/switch.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion.tsx";
import { PasswordInput } from "@/components/ui/password-input.tsx";
import type { HostRemoteDesktopTabProps } from "./shared/tab-types";
function GuacField({
form,
path,
label,
description,
type = "text",
t,
}: {
form: any;
path: string;
label: string;
description?: string;
type?: "text" | "number" | "password" | "switch";
t: (key: string) => string;
}) {
const fieldName = `guacamoleConfig.${path}` as any;
if (type === "switch") {
return (
<FormField
control={form.control}
name={fieldName}
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{label}</FormLabel>
{description && <FormDescription>{description}</FormDescription>}
</div>
<FormControl>
<Switch
checked={!!field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
);
}
if (type === "password") {
return (
<FormField
control={form.control}
name={fieldName}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<PasswordInput
value={field.value || ""}
onChange={field.onChange}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
</FormItem>
)}
/>
);
}
return (
<FormField
control={form.control}
name={fieldName}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input
type={type}
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
type === "number"
? e.target.value === ""
? undefined
: parseInt(e.target.value)
: e.target.value,
)
}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
</FormItem>
)}
/>
);
}
function GuacSelect({
form,
path,
label,
options,
placeholder,
}: {
form: any;
path: string;
label: string;
options: { value: string; label: string }[];
placeholder?: string;
}) {
const fieldName = `guacamoleConfig.${path}` as any;
return (
<FormField
control={form.control}
name={fieldName}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<Select
value={field.value || "auto"}
onValueChange={(v) => field.onChange(v === "auto" ? "" : v)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={placeholder || label} />
</SelectTrigger>
</FormControl>
<SelectContent>
{options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
);
}
export function HostRemoteDesktopTab({
form,
connectionType,
t,
}: HostRemoteDesktopTabProps) {
const isRDP = connectionType === "rdp";
const isVNC = connectionType === "vnc";
const isTelnet = connectionType === "telnet";
return (
<div className="pt-2 space-y-4">
<Accordion
type="multiple"
defaultValue={["connection", "display"]}
className="w-full"
>
{/* Connection Settings */}
{isRDP && (
<AccordionItem value="connection">
<AccordionTrigger>{t("hosts.connectionSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<FormField
control={form.control}
name="security"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.securityMode")}</FormLabel>
<Select
value={field.value || "any"}
onValueChange={field.onChange}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="any">Any</SelectItem>
<SelectItem value="nla">NLA</SelectItem>
<SelectItem value="nla-ext">NLA Extended</SelectItem>
<SelectItem value="tls">TLS</SelectItem>
<SelectItem value="vmconnect">VMConnect</SelectItem>
<SelectItem value="rdp">RDP</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="ignoreCert"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.ignoreCert")}</FormLabel>
<FormDescription>
{t("hosts.ignoreCertDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={!!field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</AccordionContent>
</AccordionItem>
)}
{/* Display Settings */}
<AccordionItem value="display">
<AccordionTrigger>{t("hosts.displaySettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
{!isTelnet && (
<GuacSelect
form={form}
path="color-depth"
label={t("hosts.colorDepth")}
options={[
{ value: "auto", label: "Auto" },
{ value: "8", label: "8-bit" },
{ value: "16", label: "16-bit" },
{ value: "24", label: "24-bit" },
{ value: "32", label: "32-bit" },
]}
/>
)}
<div className="grid grid-cols-2 gap-4">
<GuacField
form={form}
path="width"
label={t("hosts.width")}
type="number"
t={t}
/>
<GuacField
form={form}
path="height"
label={t("hosts.height")}
type="number"
t={t}
/>
</div>
{!isTelnet && (
<>
<GuacField
form={form}
path="dpi"
label={t("hosts.dpi")}
type="number"
t={t}
/>
<GuacSelect
form={form}
path="resize-method"
label={t("hosts.resizeMethod")}
options={[
{ value: "auto", label: "Auto" },
{ value: "display-update", label: "Display Update" },
{ value: "reconnect", label: "Reconnect" },
]}
/>
<GuacField
form={form}
path="force-lossless"
label={t("hosts.forceLossless")}
type="switch"
t={t}
/>
</>
)}
</AccordionContent>
</AccordionItem>
{/* Telnet Terminal Settings */}
{isTelnet && (
<AccordionItem value="telnet-terminal">
<AccordionTrigger>
{t("hosts.telnetTerminalSettings")}
</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacSelect
form={form}
path="terminal-type"
label={t("hosts.terminalType")}
options={[
{ value: "auto", label: "Auto" },
{ value: "xterm", label: "xterm" },
{ value: "xterm-256color", label: "xterm-256color" },
{ value: "vt100", label: "VT100" },
{ value: "vt220", label: "VT220" },
]}
/>
<GuacField
form={form}
path="font-name"
label={t("hosts.guacFontName")}
t={t}
/>
<GuacField
form={form}
path="font-size"
label={t("hosts.guacFontSize")}
type="number"
t={t}
/>
<GuacSelect
form={form}
path="color-scheme"
label={t("hosts.guacColorScheme")}
options={[
{ value: "auto", label: "Auto" },
{ value: "black-white", label: "Black on White" },
{ value: "white-black", label: "White on Black" },
{ value: "gray-black", label: "Gray on Black" },
{ value: "green-black", label: "Green on Black" },
]}
/>
<GuacSelect
form={form}
path="backspace"
label={t("hosts.guacBackspaceKey")}
options={[
{ value: "auto", label: "Auto" },
{ value: "127", label: "DEL (127)" },
{ value: "8", label: "BS (8)" },
]}
/>
</AccordionContent>
</AccordionItem>
)}
{/* Audio Settings */}
{(isRDP || isVNC) && (
<AccordionItem value="audio">
<AccordionTrigger>{t("hosts.audioSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="disable-audio"
label={t("hosts.disableAudio")}
type="switch"
t={t}
/>
{isRDP && (
<GuacField
form={form}
path="enable-audio-input"
label={t("hosts.enableAudioInput")}
type="switch"
t={t}
/>
)}
</AccordionContent>
</AccordionItem>
)}
{/* RDP Performance */}
{isRDP && (
<AccordionItem value="performance">
<AccordionTrigger>{t("hosts.rdpPerformance")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="enable-wallpaper"
label={t("hosts.enableWallpaper")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-theming"
label={t("hosts.enableTheming")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-font-smoothing"
label={t("hosts.enableFontSmoothing")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-full-window-drag"
label={t("hosts.enableFullWindowDrag")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-desktop-composition"
label={t("hosts.enableDesktopComposition")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-menu-animations"
label={t("hosts.enableMenuAnimations")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-bitmap-caching"
label={t("hosts.disableBitmapCaching")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-offscreen-caching"
label={t("hosts.disableOffscreenCaching")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-glyph-caching"
label={t("hosts.disableGlyphCaching")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-gfx"
label={t("hosts.enableGfx")}
type="switch"
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* RDP Device Redirection */}
{isRDP && (
<AccordionItem value="device-redirection">
<AccordionTrigger>{t("hosts.deviceRedirection")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="enable-printing"
label={t("hosts.enablePrinting")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-drive"
label={t("hosts.enableDrive")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="drive-name"
label={t("hosts.driveName")}
t={t}
/>
<GuacField
form={form}
path="drive-path"
label={t("hosts.drivePath")}
t={t}
/>
<GuacField
form={form}
path="create-drive-path"
label={t("hosts.createDrivePath")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-download"
label={t("hosts.disableDownload")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-upload"
label={t("hosts.disableUpload")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="enable-touch"
label={t("hosts.enableTouch")}
type="switch"
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* RDP Session */}
{isRDP && (
<AccordionItem value="session">
<AccordionTrigger>{t("hosts.rdpSession")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="client-name"
label={t("hosts.clientName")}
t={t}
/>
<GuacField
form={form}
path="console"
label={t("hosts.consoleSession")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="initial-program"
label={t("hosts.initialProgram")}
t={t}
/>
<GuacSelect
form={form}
path="server-layout"
label={t("hosts.serverLayout")}
options={[
{ value: "auto", label: "Auto" },
{ value: "en-us-qwerty", label: "English (US) QWERTY" },
{ value: "en-gb-qwerty", label: "English (UK) QWERTY" },
{ value: "de-de-qwertz", label: "German QWERTZ" },
{ value: "fr-fr-azerty", label: "French AZERTY" },
{ value: "it-it-qwerty", label: "Italian QWERTY" },
{ value: "sv-se-qwerty", label: "Swedish QWERTY" },
{ value: "ja-jp-qwerty", label: "Japanese QWERTY" },
{ value: "pt-br-qwerty", label: "Portuguese (BR) QWERTY" },
{ value: "es-es-qwerty", label: "Spanish QWERTY" },
{ value: "failsafe", label: "Failsafe (Unicode)" },
]}
/>
<GuacField
form={form}
path="timezone"
label={t("hosts.timezone")}
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* RDP Gateway */}
{isRDP && (
<AccordionItem value="gateway">
<AccordionTrigger>{t("hosts.gatewaySettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="gateway-hostname"
label={t("hosts.gatewayHostname")}
t={t}
/>
<GuacField
form={form}
path="gateway-port"
label={t("hosts.gatewayPort")}
type="number"
t={t}
/>
<GuacField
form={form}
path="gateway-username"
label={t("hosts.gatewayUsername")}
t={t}
/>
<GuacField
form={form}
path="gateway-password"
label={t("hosts.gatewayPassword")}
type="password"
t={t}
/>
<GuacField
form={form}
path="gateway-domain"
label={t("hosts.gatewayDomain")}
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* RDP RemoteApp */}
{isRDP && (
<AccordionItem value="remoteapp">
<AccordionTrigger>{t("hosts.remoteApp")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="remote-app"
label={t("hosts.remoteAppProgram")}
t={t}
/>
<GuacField
form={form}
path="remote-app-dir"
label={t("hosts.remoteAppDir")}
t={t}
/>
<GuacField
form={form}
path="remote-app-args"
label={t("hosts.remoteAppArgs")}
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* Clipboard */}
<AccordionItem value="clipboard">
<AccordionTrigger>{t("hosts.clipboardSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacSelect
form={form}
path="normalize-clipboard"
label={t("hosts.normalizeClipboard")}
options={[
{ value: "auto", label: "Auto" },
{ value: "preserve", label: "Preserve" },
{ value: "unix", label: "Unix (LF)" },
{ value: "windows", label: "Windows (CRLF)" },
]}
/>
<GuacField
form={form}
path="disable-copy"
label={t("hosts.disableCopy")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="disable-paste"
label={t("hosts.disablePaste")}
type="switch"
t={t}
/>
</AccordionContent>
</AccordionItem>
{/* VNC Specific */}
{isVNC && (
<AccordionItem value="vnc-specific">
<AccordionTrigger>{t("hosts.vncSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacSelect
form={form}
path="cursor"
label={t("hosts.cursorMode")}
options={[
{ value: "auto", label: "Auto" },
{ value: "local", label: "Local" },
{ value: "remote", label: "Remote" },
]}
/>
<GuacField
form={form}
path="swap-red-blue"
label={t("hosts.swapRedBlue")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="read-only"
label={t("hosts.readOnly")}
type="switch"
t={t}
/>
</AccordionContent>
</AccordionItem>
)}
{/* Recording */}
<AccordionItem value="recording">
<AccordionTrigger>{t("hosts.recordingSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="recording-path"
label={t("hosts.recordingPath")}
t={t}
/>
<GuacField
form={form}
path="recording-name"
label={t("hosts.recordingName")}
t={t}
/>
<GuacField
form={form}
path="create-recording-path"
label={t("hosts.createRecordingPath")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="recording-exclude-output"
label={t("hosts.excludeOutput")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="recording-exclude-mouse"
label={t("hosts.excludeMouse")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="recording-include-keys"
label={t("hosts.includeKeys")}
type="switch"
t={t}
/>
</AccordionContent>
</AccordionItem>
{/* Wake-on-LAN */}
<AccordionItem value="wol">
<AccordionTrigger>{t("hosts.wakeOnLan")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<GuacField
form={form}
path="wol-send-packet"
label={t("hosts.sendWolPacket")}
type="switch"
t={t}
/>
<GuacField
form={form}
path="wol-mac-addr"
label={t("hosts.wolMacAddr")}
t={t}
/>
<GuacField
form={form}
path="wol-broadcast-addr"
label={t("hosts.wolBroadcastAddr")}
t={t}
/>
<GuacField
form={form}
path="wol-udp-port"
label={t("hosts.wolUdpPort")}
type="number"
t={t}
/>
<GuacField
form={form}
path="wol-wait-time"
label={t("hosts.wolWaitTime")}
type="number"
t={t}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}
@@ -0,0 +1,143 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form.tsx";
import { Input } from "@/components/ui/input.tsx";
import { Switch } from "@/components/ui/switch.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx";
import type { HostStatisticsTabProps } from "./shared/tab-types";
export function HostStatusTab({
form,
statusIntervalUnit,
setStatusIntervalUnit,
t,
}: Pick<
HostStatisticsTabProps,
"form" | "statusIntervalUnit" | "setStatusIntervalUnit" | "t"
>) {
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="space-y-3">
<FormField
control={form.control}
name="statsConfig.statusCheckEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.statusCheckEnabled")}</FormLabel>
<FormDescription>
{t("hosts.statusCheckEnabledDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch("statsConfig.statusCheckEnabled") && (
<>
<FormField
control={form.control}
name="statsConfig.useGlobalStatusInterval"
render={({ field }) => (
<FormItem className="flex flex-row items-center gap-2 space-y-0">
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="font-normal">
{t("hosts.useGlobalStatusInterval")}
</FormLabel>
</FormItem>
)}
/>
{form.watch("statsConfig.useGlobalStatusInterval") === false && (
<FormField
control={form.control}
name="statsConfig.statusCheckInterval"
render={({ field }) => {
const displayValue =
statusIntervalUnit === "minutes"
? Math.round((field.value || 30) / 60)
: field.value || 30;
const handleIntervalChange = (value: string) => {
const numValue = parseInt(value) || 0;
const seconds =
statusIntervalUnit === "minutes"
? numValue * 60
: numValue;
field.onChange(seconds);
};
return (
<FormItem>
<FormLabel>{t("hosts.statusCheckInterval")}</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
type="number"
value={displayValue}
onChange={(e) =>
handleIntervalChange(e.target.value)
}
className="flex-1"
/>
</FormControl>
<Select
value={statusIntervalUnit}
onValueChange={(value: "seconds" | "minutes") => {
setStatusIntervalUnit(value);
const currentSeconds = field.value || 30;
if (value === "minutes") {
const minutes = Math.round(currentSeconds / 60);
field.onChange(minutes * 60);
}
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormDescription>
{t("hosts.statusCheckIntervalDesc")}
</FormDescription>
</FormItem>
);
}}
/>
)}
</>
)}
</div>
</div>
</div>
);
}
@@ -4,6 +4,7 @@ import type { SSHHost, Credential } from "@/types";
export interface HostGeneralTabProps {
form: UseFormReturn<FormData>;
connectionType: "ssh" | "rdp" | "vnc" | "telnet";
authTab: "password" | "key" | "credential" | "none";
setAuthTab: (value: "password" | "key" | "credential" | "none") => void;
keyInputMethod: "upload" | "paste";
@@ -75,6 +76,12 @@ export interface HostStatisticsTabProps {
t: (key: string) => string;
}
export interface HostRemoteDesktopTabProps {
form: UseFormReturn<FormData>;
connectionType: "rdp" | "vnc" | "telnet";
t: (key: string) => string;
}
export interface HostSharingTabProps {
hostId: number | undefined;
isNewHost: boolean;