mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
The session recording section offers a recording path, a filename template and four content toggles, but the backend overwrote five of the six on every connection. A host could set none of them and get no indication why. Location and filename genuinely are not the host's to choose — recordings are indexed by them for playback and the backend refuses to read outside its recordings directory — so drop those two inputs rather than keep pretending they apply. The content flags are a host-level decision, so default them instead of forcing them. That still leaves the reported case, where guacd writes the file somewhere the backend cannot see it. The warning now reports both paths and names the two env vars that align them, which is otherwise guesswork for a split-container setup. Closes Termix-SSH/Support#1041
1884 lines
74 KiB
TypeScript
1884 lines
74 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Input } from "@/components/input";
|
|
import { PasswordInput } from "@/components/password-input";
|
|
import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card";
|
|
import type { Host } from "@/types/ui-types";
|
|
import {
|
|
Activity,
|
|
Copy,
|
|
Globe,
|
|
Monitor,
|
|
Network,
|
|
Server,
|
|
Settings,
|
|
Shield,
|
|
Terminal,
|
|
Zap,
|
|
Cpu,
|
|
} from "lucide-react";
|
|
import type { HostEditorForm } from "./HostEditorData";
|
|
|
|
type HostEditorSetField = <K extends keyof HostEditorForm>(
|
|
key: K,
|
|
value: HostEditorForm[K],
|
|
) => void;
|
|
|
|
type GuacFieldSetter = (key: string, value: unknown) => void;
|
|
|
|
function DocsLinkAction({ href, label }: { href: string; label: string }) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-[10px] text-accent-brand hover:underline normal-case tracking-normal font-normal"
|
|
>
|
|
{label}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
export function HostEditorRdpTab({
|
|
form,
|
|
setField,
|
|
setGuacField,
|
|
host,
|
|
credentials,
|
|
}: {
|
|
form: HostEditorForm;
|
|
setField: HostEditorSetField;
|
|
setGuacField: GuacFieldSetter;
|
|
host?: Host | null;
|
|
credentials?: { id: string; name: string; username: string }[];
|
|
}) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<SectionCard
|
|
title={t("hosts.guac.connection")}
|
|
icon={<Globe className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.rdpPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="3389"
|
|
value={form.rdpPort}
|
|
onChange={(e) => setField("rdpPort", Number(e.target.value))}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.statusChecksLabel")}
|
|
icon={<Activity className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.enableStatusChecks")}
|
|
description={t("hosts.enableStatusChecksDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.statusCheckEnabled}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckEnabled: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
{form.statsConfig.statusCheckEnabled && (
|
|
<SettingRow
|
|
label={t("hosts.useGlobalInterval")}
|
|
description={t("hosts.useGlobalIntervalDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.useGlobalStatusInterval}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
useGlobalStatusInterval: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
{form.statsConfig.statusCheckEnabled &&
|
|
!form.statsConfig.useGlobalStatusInterval && (
|
|
<SettingRow
|
|
label={t("hosts.checkIntervalS")}
|
|
description={t("hosts.checkIntervalDesc")}
|
|
>
|
|
<Input
|
|
type="number"
|
|
value={form.statsConfig.statusCheckInterval}
|
|
onChange={(e) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckInterval: Number(e.target.value),
|
|
})
|
|
}
|
|
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.guac.guacdProxy")}
|
|
icon={<Cpu className="size-3.5" />}
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdHostname")}
|
|
</label>
|
|
<Input
|
|
placeholder={t("hosts.guac.guacdHostnamePlaceholder")}
|
|
value={(form.guacamoleConfig["guacd-hostname"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-hostname", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="4822"
|
|
value={(form.guacamoleConfig["guacd-port"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-port", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
<p className="col-span-full text-[10px] text-muted-foreground -mt-2">
|
|
{t("hosts.guac.guacdProxyDesc")}
|
|
</p>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.authentication")}
|
|
icon={<Shield className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.authMethod")}
|
|
</label>
|
|
<div className="flex gap-2">
|
|
{(
|
|
[
|
|
"direct",
|
|
...(credentials && credentials.length > 0
|
|
? (["credential"] as const)
|
|
: []),
|
|
"none",
|
|
] as const
|
|
).map((m) => (
|
|
<button
|
|
key={m}
|
|
type="button"
|
|
onClick={() => setField("rdpAuthType", m)}
|
|
className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest border transition-colors ${
|
|
form.rdpAuthType === m
|
|
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
|
: "border-border text-muted-foreground hover:text-foreground"
|
|
}`}
|
|
>
|
|
{t(
|
|
`hosts.guac.authType${m.charAt(0).toUpperCase() + m.slice(1)}`,
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{form.rdpAuthType === "none" ? (
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{t("hosts.guac.authTypeNoneDesc")}
|
|
</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{form.rdpAuthType === "credential" &&
|
|
credentials &&
|
|
credentials.length > 0 ? (
|
|
<div className="flex flex-col gap-1.5 col-span-full">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.storedCredential")}
|
|
</label>
|
|
<select
|
|
value={form.rdpCredentialId}
|
|
onChange={(e) =>
|
|
setField("rdpCredentialId", e.target.value)
|
|
}
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
>
|
|
<option value="">{t("hosts.guac.selectCredential")}</option>
|
|
{credentials.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.username ? `${c.name} (${c.username})` : c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.username")}
|
|
</label>
|
|
<Input
|
|
placeholder="Administrator"
|
|
value={form.rdpUser}
|
|
onChange={(e) => setField("rdpUser", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.password")}
|
|
</label>
|
|
<PasswordInput
|
|
className="h-8 text-xs pr-8"
|
|
placeholder={
|
|
form.rdpPassword === "existing_rdp_password"
|
|
? t("hosts.guac.passwordSaved")
|
|
: "••••••••"
|
|
}
|
|
value={
|
|
form.rdpPassword === "existing_rdp_password"
|
|
? ""
|
|
: form.rdpPassword
|
|
}
|
|
onFocus={() => {
|
|
if (form.rdpPassword === "existing_rdp_password")
|
|
setField("rdpPassword", "");
|
|
}}
|
|
onChange={(e) => setField("rdpPassword", e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.domain")}
|
|
</label>
|
|
<Input
|
|
placeholder="WORKGROUP"
|
|
value={form.domain}
|
|
onChange={(e) => setField("domain", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.connectionSettings")}
|
|
icon={<Shield className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.securityMode")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.security ?? "any"}
|
|
onChange={(e) => setField("security", e.target.value)}
|
|
>
|
|
<option value="any">Any</option>
|
|
<option value="nla">NLA</option>
|
|
<option value="nla-ext">NLA Extended</option>
|
|
<option value="tls">TLS</option>
|
|
<option value="vmconnect">VMConnect</option>
|
|
<option value="rdp">RDP</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.ignoreCertificate")}
|
|
description={t("hosts.guac.ignoreCertificateDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.ignoreCert}
|
|
onChange={(v) => setField("ignoreCert", v)}
|
|
/>
|
|
</SettingRow>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.loadBalanceInfo")}
|
|
</label>
|
|
<Input
|
|
placeholder="tsv://MS Terminal Services Plugin.1.CollectionName"
|
|
value={
|
|
(form.guacamoleConfig["load-balance-info"] as string) ?? ""
|
|
}
|
|
onChange={(e) =>
|
|
setGuacField("load-balance-info", e.target.value)
|
|
}
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{t("hosts.guac.loadBalanceInfoDesc")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.displaySettings")}
|
|
icon={<Monitor className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.colorDepth")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["color-depth"] ?? "auto"}
|
|
onChange={(e) => setGuacField("color-depth", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="8">8-bit</option>
|
|
<option value="16">16-bit</option>
|
|
<option value="24">24-bit</option>
|
|
<option value="32">32-bit</option>
|
|
</select>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.width")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["width"] ?? ""}
|
|
onChange={(e) => setGuacField("width", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.height")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["height"] ?? ""}
|
|
onChange={(e) => setGuacField("height", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.dpi")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="96"
|
|
value={form.guacamoleConfig["dpi"] ?? ""}
|
|
onChange={(e) => setGuacField("dpi", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.resizeMethod")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["resize-method"] ?? "auto"}
|
|
onChange={(e) => setGuacField("resize-method", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="display-update">Display Update</option>
|
|
<option value="reconnect">Reconnect</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.forceLossless")}
|
|
description={t("hosts.guac.forceLosslessDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["force-lossless"]}
|
|
onChange={(v) => setGuacField("force-lossless", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.audioSettings")}
|
|
icon={<Activity className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.guac.disableAudio")}
|
|
description={t("hosts.guac.disableAudioDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-audio"]}
|
|
onChange={(v) => setGuacField("disable-audio", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.enableAudioInput")}
|
|
description={t("hosts.guac.enableAudioInputDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-audio-input"]}
|
|
onChange={(v) => setGuacField("enable-audio-input", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.rdpPerformance")}
|
|
icon={<Zap className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.guac.wallpaper")}
|
|
description={t("hosts.guac.wallpaperDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-wallpaper"]}
|
|
onChange={(v) => setGuacField("enable-wallpaper", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.theming")}
|
|
description={t("hosts.guac.themingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-theming"]}
|
|
onChange={(v) => setGuacField("enable-theming", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.fontSmoothing")}
|
|
description={t("hosts.guac.fontSmoothingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.guacamoleConfig["enable-font-smoothing"] !== false}
|
|
onChange={(v) => setGuacField("enable-font-smoothing", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.fullWindowDrag")}
|
|
description={t("hosts.guac.fullWindowDragDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-full-window-drag"]}
|
|
onChange={(v) => setGuacField("enable-full-window-drag", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.desktopComposition")}
|
|
description={t("hosts.guac.desktopCompositionDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-desktop-composition"]}
|
|
onChange={(v) => setGuacField("enable-desktop-composition", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.menuAnimations")}
|
|
description={t("hosts.guac.menuAnimationsDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-menu-animations"]}
|
|
onChange={(v) => setGuacField("enable-menu-animations", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableBitmapCaching")}
|
|
description={t("hosts.guac.disableBitmapCachingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-bitmap-caching"]}
|
|
onChange={(v) => setGuacField("disable-bitmap-caching", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableOffscreenCaching")}
|
|
description={t("hosts.guac.disableOffscreenCachingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-offscreen-caching"]}
|
|
onChange={(v) => setGuacField("disable-offscreen-caching", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableGlyphCaching")}
|
|
description={t("hosts.guac.disableGlyphCachingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-glyph-caching"]}
|
|
onChange={(v) => setGuacField("disable-glyph-caching", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.enableGfx")}
|
|
description={t("hosts.guac.enableGfxDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.guacamoleConfig["enable-gfx"] !== false}
|
|
onChange={(v) => setGuacField("enable-gfx", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.deviceRedirection")}
|
|
icon={<Settings className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.enablePrinting")}
|
|
description={t("hosts.guac.enablePrintingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-printing"]}
|
|
onChange={(v) => setGuacField("enable-printing", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.enableDriveRedirection")}
|
|
description={t("hosts.guac.enableDriveRedirectionDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-drive"]}
|
|
onChange={(v) => setGuacField("enable-drive", v)}
|
|
/>
|
|
</SettingRow>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 border-t border-border pt-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.driveName")}
|
|
</label>
|
|
<Input
|
|
placeholder="Termix Drive"
|
|
value={form.guacamoleConfig["drive-name"] ?? ""}
|
|
onChange={(e) => setGuacField("drive-name", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.drivePath")}
|
|
</label>
|
|
<Input
|
|
placeholder="/home/user/shared"
|
|
value={form.guacamoleConfig["drive-path"] ?? ""}
|
|
onChange={(e) => setGuacField("drive-path", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.createDrivePath")}
|
|
description={t("hosts.guac.createDrivePathDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["create-drive-path"]}
|
|
onChange={(v) => setGuacField("create-drive-path", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableDownload")}
|
|
description={t("hosts.guac.disableDownloadDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-download"]}
|
|
onChange={(v) => setGuacField("disable-download", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableUpload")}
|
|
description={t("hosts.guac.disableUploadDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-upload"]}
|
|
onChange={(v) => setGuacField("disable-upload", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.enableTouch")}
|
|
description={t("hosts.guac.enableTouchDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["enable-touch"]}
|
|
onChange={(v) => setGuacField("enable-touch", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.session")}
|
|
icon={<Server className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.clientName")}
|
|
</label>
|
|
<Input
|
|
placeholder="Termix"
|
|
value={form.guacamoleConfig["client-name"] ?? ""}
|
|
onChange={(e) => setGuacField("client-name", e.target.value)}
|
|
/>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.consoleSession")}
|
|
description={t("hosts.guac.consoleSessionDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["console"]}
|
|
onChange={(v) => setGuacField("console", v)}
|
|
/>
|
|
</SettingRow>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.initialProgram")}
|
|
</label>
|
|
<Input
|
|
placeholder="e.g. cmd.exe"
|
|
value={form.guacamoleConfig["initial-program"] ?? ""}
|
|
onChange={(e) => setGuacField("initial-program", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.serverLayout")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["server-layout"] ?? "auto"}
|
|
onChange={(e) => setGuacField("server-layout", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option>en-us-qwerty</option>
|
|
<option>en-gb-qwerty</option>
|
|
<option>de-de-qwertz</option>
|
|
<option>de-ch-qwertz</option>
|
|
<option>fr-fr-azerty</option>
|
|
<option>it-it-qwerty</option>
|
|
<option>sv-se-qwerty</option>
|
|
<option>ja-jp-qwerty</option>
|
|
<option>pt-br-qwerty</option>
|
|
<option>es-es-qwerty</option>
|
|
<option>failsafe</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.timezone")}
|
|
</label>
|
|
<Input
|
|
placeholder="e.g. America/New_York"
|
|
value={form.guacamoleConfig["timezone"] ?? ""}
|
|
onChange={(e) => setGuacField("timezone", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.gateway")}
|
|
icon={<Network className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.gatewayHostname")}
|
|
</label>
|
|
<Input
|
|
placeholder="gateway.example.com"
|
|
value={form.guacamoleConfig["gateway-hostname"] ?? ""}
|
|
onChange={(e) =>
|
|
setGuacField("gateway-hostname", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.gatewayPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="443"
|
|
value={form.guacamoleConfig["gateway-port"] ?? ""}
|
|
onChange={(e) => setGuacField("gateway-port", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.gatewayUsername")}
|
|
</label>
|
|
<Input
|
|
placeholder="user"
|
|
value={form.guacamoleConfig["gateway-username"] ?? ""}
|
|
onChange={(e) =>
|
|
setGuacField("gateway-username", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.gatewayPassword")}
|
|
</label>
|
|
<PasswordInput
|
|
className="h-8 text-xs pr-8"
|
|
placeholder="••••••••"
|
|
value={form.guacamoleConfig["gateway-password"] ?? ""}
|
|
onChange={(e) =>
|
|
setGuacField("gateway-password", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5 col-span-2">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.gatewayDomain")}
|
|
</label>
|
|
<Input
|
|
placeholder="DOMAIN"
|
|
value={form.guacamoleConfig["gateway-domain"] ?? ""}
|
|
onChange={(e) => setGuacField("gateway-domain", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.remoteApp")}
|
|
icon={<Monitor className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.remoteAppProgram")}
|
|
</label>
|
|
<Input
|
|
placeholder="||MyApp"
|
|
value={form.guacamoleConfig["remote-app"] ?? ""}
|
|
onChange={(e) => setGuacField("remote-app", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.workingDirectory")}
|
|
</label>
|
|
<Input
|
|
placeholder="C:\Apps\MyApp"
|
|
value={form.guacamoleConfig["remote-app-dir"] ?? ""}
|
|
onChange={(e) => setGuacField("remote-app-dir", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.arguments")}
|
|
</label>
|
|
<Input
|
|
placeholder="--flag value"
|
|
value={form.guacamoleConfig["remote-app-args"] ?? ""}
|
|
onChange={(e) => setGuacField("remote-app-args", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.clipboard")}
|
|
icon={<Copy className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.normalizeLineEndings")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["normalize-clipboard"] ?? "auto"}
|
|
onChange={(e) =>
|
|
setGuacField("normalize-clipboard", e.target.value)
|
|
}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="preserve">Preserve</option>
|
|
<option value="unix">Unix (LF)</option>
|
|
<option value="windows">Windows (CRLF)</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableCopy")}
|
|
description={t("hosts.guac.disableCopyDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-copy"]}
|
|
onChange={(v) => setGuacField("disable-copy", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disablePaste")}
|
|
description={t("hosts.guac.disablePasteDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-paste"]}
|
|
onChange={(v) => setGuacField("disable-paste", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.sessionRecording")}
|
|
icon={<Activity className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop#session-recording"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.createPathIfMissing")}
|
|
description={t("hosts.guac.createPathIfMissingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["create-recording-path"]}
|
|
onChange={(v) => setGuacField("create-recording-path", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.excludeOutput")}
|
|
description={t("hosts.guac.excludeOutputDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-exclude-output"]}
|
|
onChange={(v) => setGuacField("recording-exclude-output", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.excludeMouse")}
|
|
description={t("hosts.guac.excludeMouseDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-exclude-mouse"]}
|
|
onChange={(v) => setGuacField("recording-exclude-mouse", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.includeKeystrokes")}
|
|
description={t("hosts.guac.includeKeystrokesDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-include-keys"]}
|
|
onChange={(v) => setGuacField("recording-include-keys", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.wakeOnLan")}
|
|
icon={<Zap className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/features/networking/wake-on-lan"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.sendWolPacket")}
|
|
description={t("hosts.guac.sendWolPacketDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["wol-send-packet"]}
|
|
onChange={(v) => setGuacField("wol-send-packet", v)}
|
|
/>
|
|
</SettingRow>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.macAddress")}
|
|
</label>
|
|
<Input
|
|
placeholder="AA:BB:CC:DD:EE:FF"
|
|
value={
|
|
form.guacamoleConfig["wol-mac-addr"] ?? host?.macAddress ?? ""
|
|
}
|
|
onChange={(e) => setGuacField("wol-mac-addr", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.broadcastAddress")}
|
|
</label>
|
|
<Input
|
|
placeholder="255.255.255.255"
|
|
value={form.guacamoleConfig["wol-broadcast-addr"] ?? ""}
|
|
onChange={(e) =>
|
|
setGuacField("wol-broadcast-addr", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.udpPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="9"
|
|
value={form.guacamoleConfig["wol-udp-port"] ?? ""}
|
|
onChange={(e) => setGuacField("wol-udp-port", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.waitTimeS")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="0"
|
|
value={form.guacamoleConfig["wol-wait-time"] ?? ""}
|
|
onChange={(e) => setGuacField("wol-wait-time", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function HostEditorVncTab({
|
|
form,
|
|
setField,
|
|
setGuacField,
|
|
host,
|
|
credentials,
|
|
}: {
|
|
form: HostEditorForm;
|
|
setField: HostEditorSetField;
|
|
setGuacField: GuacFieldSetter;
|
|
host?: Host | null;
|
|
credentials?: { id: string; name: string; username: string }[];
|
|
}) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<SectionCard
|
|
title={t("hosts.guac.connection")}
|
|
icon={<Globe className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.vncPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="5900"
|
|
value={form.vncPort}
|
|
onChange={(e) => setField("vncPort", Number(e.target.value))}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.statusChecksLabel")}
|
|
icon={<Activity className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.enableStatusChecks")}
|
|
description={t("hosts.enableStatusChecksDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.statusCheckEnabled}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckEnabled: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
{form.statsConfig.statusCheckEnabled && (
|
|
<SettingRow
|
|
label={t("hosts.useGlobalInterval")}
|
|
description={t("hosts.useGlobalIntervalDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.useGlobalStatusInterval}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
useGlobalStatusInterval: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
{form.statsConfig.statusCheckEnabled &&
|
|
!form.statsConfig.useGlobalStatusInterval && (
|
|
<SettingRow
|
|
label={t("hosts.checkIntervalS")}
|
|
description={t("hosts.checkIntervalDesc")}
|
|
>
|
|
<Input
|
|
type="number"
|
|
value={form.statsConfig.statusCheckInterval}
|
|
onChange={(e) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckInterval: Number(e.target.value),
|
|
})
|
|
}
|
|
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.guac.guacdProxy")}
|
|
icon={<Cpu className="size-3.5" />}
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdHostname")}
|
|
</label>
|
|
<Input
|
|
placeholder={t("hosts.guac.guacdHostnamePlaceholder")}
|
|
value={(form.guacamoleConfig["guacd-hostname"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-hostname", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="4822"
|
|
value={(form.guacamoleConfig["guacd-port"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-port", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
<p className="col-span-full text-[10px] text-muted-foreground -mt-2">
|
|
{t("hosts.guac.guacdProxyDesc")}
|
|
</p>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.authentication")}
|
|
icon={<Shield className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
{credentials && credentials.length > 0 && (
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.authMethod")}
|
|
</label>
|
|
<div className="flex gap-2">
|
|
{(["direct", "credential"] as const).map((m) => (
|
|
<button
|
|
key={m}
|
|
type="button"
|
|
onClick={() => setField("vncAuthType", m)}
|
|
className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest border transition-colors ${
|
|
form.vncAuthType === m
|
|
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
|
: "border-border text-muted-foreground hover:text-foreground"
|
|
}`}
|
|
>
|
|
{t(
|
|
`hosts.guac.authType${m.charAt(0).toUpperCase() + m.slice(1)}`,
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{form.vncAuthType === "credential" &&
|
|
credentials &&
|
|
credentials.length > 0 ? (
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.storedCredential")}
|
|
</label>
|
|
<select
|
|
value={form.vncCredentialId}
|
|
onChange={(e) => setField("vncCredentialId", e.target.value)}
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
>
|
|
<option value="">{t("hosts.guac.selectCredential")}</option>
|
|
{credentials.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.vncPassword")}
|
|
</label>
|
|
<PasswordInput
|
|
className="h-8 text-xs pr-8"
|
|
placeholder={
|
|
form.vncPassword === "existing_vnc_password"
|
|
? t("hosts.guac.passwordSaved")
|
|
: "••••••••"
|
|
}
|
|
value={
|
|
form.vncPassword === "existing_vnc_password"
|
|
? ""
|
|
: form.vncPassword
|
|
}
|
|
onFocus={() => {
|
|
if (form.vncPassword === "existing_vnc_password")
|
|
setField("vncPassword", "");
|
|
}}
|
|
onChange={(e) => setField("vncPassword", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.vncUsernameOptional")}
|
|
</label>
|
|
<Input
|
|
placeholder={t("hosts.guac.vncLeaveBlank")}
|
|
value={form.vncUser}
|
|
onChange={(e) => setField("vncUser", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.displaySettings")}
|
|
icon={<Monitor className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.colorDepth")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["color-depth"] ?? "auto"}
|
|
onChange={(e) => setGuacField("color-depth", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="8">8-bit</option>
|
|
<option value="16">16-bit</option>
|
|
<option value="24">24-bit</option>
|
|
<option value="32">32-bit</option>
|
|
</select>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.width")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["width"] ?? ""}
|
|
onChange={(e) => setGuacField("width", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.height")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["height"] ?? ""}
|
|
onChange={(e) => setGuacField("height", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.resizeMethod")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["resize-method"] ?? "auto"}
|
|
onChange={(e) => setGuacField("resize-method", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="display-update">Display Update</option>
|
|
<option value="reconnect">Reconnect</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.forceLossless")}
|
|
description={t("hosts.guac.forceLosslessDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["force-lossless"]}
|
|
onChange={(v) => setGuacField("force-lossless", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.audioSettings")}
|
|
icon={<Activity className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.guac.disableAudio")}
|
|
description={t("hosts.guac.disableAudioDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-audio"]}
|
|
onChange={(v) => setGuacField("disable-audio", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.vncSettings")}
|
|
icon={<Settings className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.cursorMode")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["cursor"] ?? "auto"}
|
|
onChange={(e) => setGuacField("cursor", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="local">Local</option>
|
|
<option value="remote">Remote</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.serverLayout")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["server-layout"] ?? "auto"}
|
|
onChange={(e) => setGuacField("server-layout", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option>en-us-qwerty</option>
|
|
<option>en-gb-qwerty</option>
|
|
<option>de-de-qwertz</option>
|
|
<option>de-ch-qwertz</option>
|
|
<option>fr-fr-azerty</option>
|
|
<option>it-it-qwerty</option>
|
|
<option>sv-se-qwerty</option>
|
|
<option>ja-jp-qwerty</option>
|
|
<option>pt-br-qwerty</option>
|
|
<option>es-es-qwerty</option>
|
|
<option>failsafe</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.swapRedBlue")}
|
|
description={t("hosts.guac.swapRedBlueDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["swap-red-blue"]}
|
|
onChange={(v) => setGuacField("swap-red-blue", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.readOnly")}
|
|
description={t("hosts.guac.readOnlyDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["read-only"]}
|
|
onChange={(v) => setGuacField("read-only", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.clipboard")}
|
|
icon={<Copy className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.normalizeLineEndings")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["normalize-clipboard"] ?? "auto"}
|
|
onChange={(e) =>
|
|
setGuacField("normalize-clipboard", e.target.value)
|
|
}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="preserve">Preserve</option>
|
|
<option value="unix">Unix (LF)</option>
|
|
<option value="windows">Windows (CRLF)</option>
|
|
</select>
|
|
</div>
|
|
<SettingRow
|
|
label={t("hosts.guac.disableCopy")}
|
|
description={t("hosts.guac.disableCopyDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-copy"]}
|
|
onChange={(v) => setGuacField("disable-copy", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.disablePaste")}
|
|
description={t("hosts.guac.disablePasteDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["disable-paste"]}
|
|
onChange={(v) => setGuacField("disable-paste", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.sessionRecording")}
|
|
icon={<Activity className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop#session-recording"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.createPathIfMissing")}
|
|
description={t("hosts.guac.createPathIfMissingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["create-recording-path"]}
|
|
onChange={(v) => setGuacField("create-recording-path", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.excludeOutput")}
|
|
description={t("hosts.guac.excludeOutputDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-exclude-output"]}
|
|
onChange={(v) => setGuacField("recording-exclude-output", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.excludeMouse")}
|
|
description={t("hosts.guac.excludeMouseDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-exclude-mouse"]}
|
|
onChange={(v) => setGuacField("recording-exclude-mouse", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.includeKeystrokes")}
|
|
description={t("hosts.guac.includeKeystrokesDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-include-keys"]}
|
|
onChange={(v) => setGuacField("recording-include-keys", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.wakeOnLan")}
|
|
icon={<Zap className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/features/networking/wake-on-lan"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.sendWolPacket")}
|
|
description={t("hosts.guac.sendWolPacketDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["wol-send-packet"]}
|
|
onChange={(v) => setGuacField("wol-send-packet", v)}
|
|
/>
|
|
</SettingRow>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.macAddress")}
|
|
</label>
|
|
<Input
|
|
placeholder="AA:BB:CC:DD:EE:FF"
|
|
value={
|
|
form.guacamoleConfig["wol-mac-addr"] ?? host?.macAddress ?? ""
|
|
}
|
|
onChange={(e) => setGuacField("wol-mac-addr", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.broadcastAddress")}
|
|
</label>
|
|
<Input
|
|
placeholder="255.255.255.255"
|
|
value={form.guacamoleConfig["wol-broadcast-addr"] ?? ""}
|
|
onChange={(e) =>
|
|
setGuacField("wol-broadcast-addr", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.udpPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="9"
|
|
value={form.guacamoleConfig["wol-udp-port"] ?? ""}
|
|
onChange={(e) => setGuacField("wol-udp-port", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.waitTimeS")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="0"
|
|
value={form.guacamoleConfig["wol-wait-time"] ?? ""}
|
|
onChange={(e) => setGuacField("wol-wait-time", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function HostEditorTelnetTab({
|
|
form,
|
|
setField,
|
|
setGuacField,
|
|
credentials,
|
|
}: {
|
|
form: HostEditorForm;
|
|
setField: HostEditorSetField;
|
|
setGuacField: GuacFieldSetter;
|
|
host?: Host | null;
|
|
credentials?: { id: string; name: string; username: string }[];
|
|
}) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<SectionCard
|
|
title={t("hosts.guac.connection")}
|
|
icon={<Globe className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.telnetPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="23"
|
|
value={form.telnetPort}
|
|
onChange={(e) => setField("telnetPort", Number(e.target.value))}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.statusChecksLabel")}
|
|
icon={<Activity className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-0 py-1">
|
|
<SettingRow
|
|
label={t("hosts.enableStatusChecks")}
|
|
description={t("hosts.enableStatusChecksDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.statusCheckEnabled}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckEnabled: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
{form.statsConfig.statusCheckEnabled && (
|
|
<SettingRow
|
|
label={t("hosts.useGlobalInterval")}
|
|
description={t("hosts.useGlobalIntervalDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={form.statsConfig.useGlobalStatusInterval}
|
|
onChange={(v) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
useGlobalStatusInterval: v,
|
|
})
|
|
}
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
{form.statsConfig.statusCheckEnabled &&
|
|
!form.statsConfig.useGlobalStatusInterval && (
|
|
<SettingRow
|
|
label={t("hosts.checkIntervalS")}
|
|
description={t("hosts.checkIntervalDesc")}
|
|
>
|
|
<Input
|
|
type="number"
|
|
value={form.statsConfig.statusCheckInterval}
|
|
onChange={(e) =>
|
|
setField("statsConfig", {
|
|
...form.statsConfig,
|
|
statusCheckInterval: Number(e.target.value),
|
|
})
|
|
}
|
|
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</SettingRow>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.guac.guacdProxy")}
|
|
icon={<Cpu className="size-3.5" />}
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdHostname")}
|
|
</label>
|
|
<Input
|
|
placeholder={t("hosts.guac.guacdHostnamePlaceholder")}
|
|
value={(form.guacamoleConfig["guacd-hostname"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-hostname", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.guacdPort")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="4822"
|
|
value={(form.guacamoleConfig["guacd-port"] as string) ?? ""}
|
|
onChange={(e) => setGuacField("guacd-port", e.target.value)}
|
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
</div>
|
|
<p className="col-span-full text-[10px] text-muted-foreground -mt-2">
|
|
{t("hosts.guac.guacdProxyDesc")}
|
|
</p>
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard
|
|
title={t("hosts.guac.authentication")}
|
|
icon={<Shield className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
{credentials && credentials.length > 0 && (
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.authMethod")}
|
|
</label>
|
|
<div className="flex gap-2">
|
|
{(["direct", "credential"] as const).map((m) => (
|
|
<button
|
|
key={m}
|
|
type="button"
|
|
onClick={() => setField("telnetAuthType", m)}
|
|
className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest border transition-colors ${
|
|
form.telnetAuthType === m
|
|
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
|
: "border-border text-muted-foreground hover:text-foreground"
|
|
}`}
|
|
>
|
|
{t(
|
|
`hosts.guac.authType${m.charAt(0).toUpperCase() + m.slice(1)}`,
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{form.telnetAuthType === "credential" &&
|
|
credentials &&
|
|
credentials.length > 0 ? (
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.storedCredential")}
|
|
</label>
|
|
<select
|
|
value={form.telnetCredentialId}
|
|
onChange={(e) => setField("telnetCredentialId", e.target.value)}
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
>
|
|
<option value="">{t("hosts.guac.selectCredential")}</option>
|
|
{credentials.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.username ? `${c.name} (${c.username})` : c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.username")}
|
|
</label>
|
|
<Input
|
|
placeholder="admin"
|
|
value={form.telnetUser}
|
|
onChange={(e) => setField("telnetUser", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.password")}
|
|
</label>
|
|
<PasswordInput
|
|
className="h-8 text-xs pr-8"
|
|
placeholder={
|
|
form.telnetPassword === "existing_telnet_password"
|
|
? t("hosts.guac.passwordSaved")
|
|
: "••••••••"
|
|
}
|
|
value={
|
|
form.telnetPassword === "existing_telnet_password"
|
|
? ""
|
|
: form.telnetPassword
|
|
}
|
|
onFocus={() => {
|
|
if (form.telnetPassword === "existing_telnet_password")
|
|
setField("telnetPassword", "");
|
|
}}
|
|
onChange={(e) => setField("telnetPassword", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.displaySettings")}
|
|
icon={<Monitor className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.width")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["width"] ?? ""}
|
|
onChange={(e) => setGuacField("width", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.height")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Auto"
|
|
value={form.guacamoleConfig["height"] ?? ""}
|
|
onChange={(e) => setGuacField("height", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.terminalSettings")}
|
|
icon={<Terminal className="size-3.5" />}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.terminalType")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["terminal-type"] ?? "auto"}
|
|
onChange={(e) => setGuacField("terminal-type", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="xterm">xterm</option>
|
|
<option value="xterm-256color">xterm-256color</option>
|
|
<option value="vt100">VT100</option>
|
|
<option value="vt220">VT220</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.fontName")}
|
|
</label>
|
|
<Input
|
|
placeholder="monospace"
|
|
value={form.guacamoleConfig["font-name"] ?? ""}
|
|
onChange={(e) => setGuacField("font-name", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.fontSize")}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={form.guacamoleConfig["font-size"] ?? 12}
|
|
onChange={(e) =>
|
|
setGuacField("font-size", Number(e.target.value))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.colorScheme")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["color-scheme"] ?? "auto"}
|
|
onChange={(e) => setGuacField("color-scheme", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="black-white">Black on White</option>
|
|
<option value="white-black">White on Black</option>
|
|
<option value="gray-black">Gray on Black</option>
|
|
<option value="green-black">Green on Black</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t("hosts.guac.backspaceKey")}
|
|
</label>
|
|
<select
|
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
value={form.guacamoleConfig["backspace"] ?? "auto"}
|
|
onChange={(e) => setGuacField("backspace", e.target.value)}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="127">DEL (127)</option>
|
|
<option value="8">BS (8)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard
|
|
title={t("hosts.guac.sessionRecording")}
|
|
icon={<Activity className="size-3.5" />}
|
|
action={
|
|
<DocsLinkAction
|
|
href="https://docs.termix.site/setup/remote-desktop#session-recording"
|
|
label={t("hosts.docsLink")}
|
|
/>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4 py-3">
|
|
<SettingRow
|
|
label={t("hosts.guac.createPathIfMissing")}
|
|
description={t("hosts.guac.createPathIfMissingDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["create-recording-path"]}
|
|
onChange={(v) => setGuacField("create-recording-path", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.excludeOutput")}
|
|
description={t("hosts.guac.excludeOutputDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-exclude-output"]}
|
|
onChange={(v) => setGuacField("recording-exclude-output", v)}
|
|
/>
|
|
</SettingRow>
|
|
<SettingRow
|
|
label={t("hosts.guac.includeKeystrokes")}
|
|
description={t("hosts.guac.includeKeystrokesDesc")}
|
|
>
|
|
<FakeSwitch
|
|
checked={!!form.guacamoleConfig["recording-include-keys"]}
|
|
onChange={(v) => setGuacField("recording-include-keys", v)}
|
|
/>
|
|
</SettingRow>
|
|
</div>
|
|
</SectionCard>
|
|
</>
|
|
);
|
|
}
|