mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 18:58:52 +00:00
v2.0.0 (#621)
* 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:
co-authored by
Wesley Reid
Tran Trung Kien
junu
송준우
SlimGary
Nunzio Marfè
Claude Sonnet 4.5
parent
a255a08903
commit
e9e30cd318
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user