mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
feat: terminal theme improvemnts, file manager improvements, host manager improvements (ready for initial user testing?)
This commit is contained in:
@@ -739,7 +739,7 @@ router.post(
|
|||||||
portKnockSequence: portKnockSequence
|
portKnockSequence: portKnockSequence
|
||||||
? JSON.stringify(portKnockSequence)
|
? JSON.stringify(portKnockSequence)
|
||||||
: null,
|
: null,
|
||||||
enableSsh: enableSsh !== false ? 1 : 0,
|
enableSsh: enableSsh ? 1 : 0,
|
||||||
enableRdp: enableRdp ? 1 : 0,
|
enableRdp: enableRdp ? 1 : 0,
|
||||||
enableVnc: enableVnc ? 1 : 0,
|
enableVnc: enableVnc ? 1 : 0,
|
||||||
enableTelnet: enableTelnet ? 1 : 0,
|
enableTelnet: enableTelnet ? 1 : 0,
|
||||||
@@ -1274,7 +1274,7 @@ router.put(
|
|||||||
portKnockSequence: portKnockSequence
|
portKnockSequence: portKnockSequence
|
||||||
? JSON.stringify(portKnockSequence)
|
? JSON.stringify(portKnockSequence)
|
||||||
: null,
|
: null,
|
||||||
enableSsh: enableSsh !== false ? 1 : 0,
|
enableSsh: enableSsh ? 1 : 0,
|
||||||
enableRdp: enableRdp ? 1 : 0,
|
enableRdp: enableRdp ? 1 : 0,
|
||||||
enableVnc: enableVnc ? 1 : 0,
|
enableVnc: enableVnc ? 1 : 0,
|
||||||
enableTelnet: enableTelnet ? 1 : 0,
|
enableTelnet: enableTelnet ? 1 : 0,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type Host = {
|
|||||||
credentialId?: string;
|
credentialId?: string;
|
||||||
overrideCredentialUsername?: boolean;
|
overrideCredentialUsername?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
hasPassword?: boolean;
|
||||||
|
hasKey?: boolean;
|
||||||
key?: string;
|
key?: string;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
keyType?: string;
|
keyType?: string;
|
||||||
|
|||||||
+24
-33
@@ -265,6 +265,11 @@ export function AppShell({
|
|||||||
loadHosts();
|
loadHosts();
|
||||||
}, [loadHosts]);
|
}, [loadHosts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("termix:hosts-changed", loadHosts);
|
||||||
|
return () => window.removeEventListener("termix:hosts-changed", loadHosts);
|
||||||
|
}, [loadHosts]);
|
||||||
|
|
||||||
// Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings)
|
// Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allHosts.length === 0) return;
|
if (allHosts.length === 0) return;
|
||||||
@@ -293,37 +298,29 @@ export function AppShell({
|
|||||||
// ─── Tab management ──────────────────────────────────────────────────────
|
// ─── Tab management ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function openTab(host: Host, type: TabType) {
|
function openTab(host: Host, type: TabType) {
|
||||||
const same = tabs.filter(
|
|
||||||
(t) => t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
|
|
||||||
);
|
|
||||||
if (same.length === 0) {
|
|
||||||
const tabId = `${host.name}-${type}`;
|
|
||||||
const ref = type === "terminal" ? createRef() : undefined;
|
|
||||||
if (ref) terminalRefs.current.set(tabId, ref);
|
|
||||||
const tab = { id: tabId, type, label: host.name, host, terminalRef: ref };
|
|
||||||
setTabs((prev) => [...prev, tab]);
|
|
||||||
setActiveTabId(tab.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tabId = `${host.name}-${type}-${Date.now()}`;
|
const tabId = `${host.name}-${type}-${Date.now()}`;
|
||||||
const ref = type === "terminal" ? createRef() : undefined;
|
const ref = type === "terminal" ? createRef() : undefined;
|
||||||
if (ref) terminalRefs.current.set(tabId, ref);
|
if (ref) terminalRefs.current.set(tabId, ref);
|
||||||
const tab = {
|
|
||||||
id: tabId,
|
|
||||||
type,
|
|
||||||
label: `${host.name} (${same.length + 1})`,
|
|
||||||
host,
|
|
||||||
terminalRef: ref,
|
|
||||||
};
|
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
const next = prev.map((t) =>
|
const same = prev.filter(
|
||||||
t.id === same[0].id && !/\(\d+\)$/.test(t.label)
|
(t) =>
|
||||||
? { ...t, label: `${host.name} (1)`, host }
|
t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
|
||||||
: t,
|
|
||||||
);
|
);
|
||||||
return [...next, tab];
|
const label =
|
||||||
|
same.length === 0 ? host.name : `${host.name} (${same.length + 1})`;
|
||||||
|
|
||||||
|
// Retrofit the first duplicate's label to "(1)" if needed
|
||||||
|
const next =
|
||||||
|
same.length === 1 && !/\(\d+\)$/.test(same[0].label)
|
||||||
|
? prev.map((t) =>
|
||||||
|
t.id === same[0].id ? { ...t, label: `${host.name} (1)` } : t,
|
||||||
|
)
|
||||||
|
: prev;
|
||||||
|
|
||||||
|
return [...next, { id: tabId, type, label, host, terminalRef: ref }];
|
||||||
});
|
});
|
||||||
setActiveTabId(tab.id);
|
setActiveTabId(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectHost(host: Host, preferredType?: TabType) {
|
function connectHost(host: Host, preferredType?: TabType) {
|
||||||
@@ -674,11 +671,7 @@ export function AppShell({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */}
|
{/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */}
|
||||||
{(() => {
|
{tabs
|
||||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
|
||||||
const nonTerminalActive =
|
|
||||||
activeTab && activeTab.type !== "terminal";
|
|
||||||
return tabs
|
|
||||||
.filter((tab) => tab.type === "terminal")
|
.filter((tab) => tab.type === "terminal")
|
||||||
.map((tab) => {
|
.map((tab) => {
|
||||||
const visible = tab.id === activeTabId;
|
const visible = tab.id === activeTabId;
|
||||||
@@ -687,7 +680,6 @@ export function AppShell({
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
className="absolute inset-0 overflow-hidden"
|
className="absolute inset-0 overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
display: nonTerminalActive ? "none" : undefined,
|
|
||||||
visibility: visible ? "visible" : "hidden",
|
visibility: visible ? "visible" : "hidden",
|
||||||
pointerEvents: visible ? "auto" : "none",
|
pointerEvents: visible ? "auto" : "none",
|
||||||
zIndex: visible ? 1 : 0,
|
zIndex: visible ? 1 : 0,
|
||||||
@@ -702,8 +694,7 @@ export function AppShell({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
})}
|
||||||
})()}
|
|
||||||
{/* Non-terminal tabs: absolutely positioned above terminals when active */}
|
{/* Non-terminal tabs: absolutely positioned above terminals when active */}
|
||||||
{tabs
|
{tabs
|
||||||
.filter((tab) => tab.type !== "terminal")
|
.filter((tab) => tab.type !== "terminal")
|
||||||
|
|||||||
@@ -37,21 +37,23 @@ function Slider({
|
|||||||
<SliderPrimitive.Track
|
<SliderPrimitive.Track
|
||||||
data-slot="slider-track"
|
data-slot="slider-track"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
"bg-input relative grow overflow-hidden rounded-none data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Range
|
<SliderPrimitive.Range
|
||||||
data-slot="slider-range"
|
data-slot="slider-range"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
"absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||||
)}
|
)}
|
||||||
|
style={{ backgroundColor: "var(--accent-brand)" }}
|
||||||
/>
|
/>
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
{Array.from({ length: _values.length }, (_, index) => (
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
<SliderPrimitive.Thumb
|
<SliderPrimitive.Thumb
|
||||||
data-slot="slider-thumb"
|
data-slot="slider-thumb"
|
||||||
key={index}
|
key={index}
|
||||||
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-elevated shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
className="block size-3.5 shrink-0 rounded-none shadow-sm transition-[color,box-shadow] hover:ring-4 hover:ring-[#f59145]/40 focus-visible:ring-4 focus-visible:ring-[#f59145]/40 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
style={{ backgroundColor: "var(--accent-brand)" }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
|
|||||||
@@ -557,10 +557,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading && currentLoadingPathRef.current !== path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolvedPath = path;
|
let resolvedPath = path;
|
||||||
if (path.includes("$") || path.startsWith("~")) {
|
if (path.includes("$") || path.startsWith("~")) {
|
||||||
resolvedPath = await resolveSSHPath(sshSessionId, path);
|
resolvedPath = await resolveSSHPath(sshSessionId, path);
|
||||||
@@ -711,6 +707,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
|
|
||||||
const navigateTo = useCallback(
|
const navigateTo = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
|
if (sshSessionId) setIsLoading(true);
|
||||||
setCurrentPath(path);
|
setCurrentPath(path);
|
||||||
setNavHistory((prev) => {
|
setNavHistory((prev) => {
|
||||||
const next = [...prev.slice(0, navIndex + 1), path];
|
const next = [...prev.slice(0, navIndex + 1), path];
|
||||||
@@ -718,24 +715,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[navIndex],
|
[navIndex, sshSessionId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goBack = useCallback(() => {
|
const goBack = useCallback(() => {
|
||||||
if (navIndex > 0) {
|
if (navIndex > 0) {
|
||||||
|
if (sshSessionId) setIsLoading(true);
|
||||||
const newIndex = navIndex - 1;
|
const newIndex = navIndex - 1;
|
||||||
setNavIndex(newIndex);
|
setNavIndex(newIndex);
|
||||||
setCurrentPath(navHistory[newIndex]);
|
setCurrentPath(navHistory[newIndex]);
|
||||||
}
|
}
|
||||||
}, [navIndex, navHistory]);
|
}, [navIndex, navHistory, sshSessionId]);
|
||||||
|
|
||||||
const goForward = useCallback(() => {
|
const goForward = useCallback(() => {
|
||||||
if (navIndex < navHistory.length - 1) {
|
if (navIndex < navHistory.length - 1) {
|
||||||
|
if (sshSessionId) setIsLoading(true);
|
||||||
const newIndex = navIndex + 1;
|
const newIndex = navIndex + 1;
|
||||||
setNavIndex(newIndex);
|
setNavIndex(newIndex);
|
||||||
setCurrentPath(navHistory[newIndex]);
|
setCurrentPath(navHistory[newIndex]);
|
||||||
}
|
}
|
||||||
}, [navIndex, navHistory]);
|
}, [navIndex, navHistory, sshSessionId]);
|
||||||
|
|
||||||
const goUp = useCallback(() => {
|
const goUp = useCallback(() => {
|
||||||
if (currentPath === "/") return;
|
if (currentPath === "/") return;
|
||||||
@@ -1291,6 +1290,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
|
|
||||||
async function handleFileOpen(file: FileItem) {
|
async function handleFileOpen(file: FileItem) {
|
||||||
if (file.type === "directory") {
|
if (file.type === "directory") {
|
||||||
|
if (sshSessionId) setIsLoading(true);
|
||||||
setCurrentPath(file.path);
|
setCurrentPath(file.path);
|
||||||
} else if (file.type === "link") {
|
} else if (file.type === "link") {
|
||||||
await handleSymlinkClick(file);
|
await handleSymlinkClick(file);
|
||||||
@@ -2599,11 +2599,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleRefreshDirectory}
|
onClick={handleRefreshDirectory}
|
||||||
disabled={isLoading}
|
|
||||||
className="size-8 rounded-none"
|
className="size-8 rounded-none"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`size-4 ${isLoading ? "animate-spin" : ""}`}
|
className={`size-4 ${isLoading && !!sshSessionId ? "animate-spin [animation-duration:0.5s]" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2878,8 +2877,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
onFileOpen={handleFileOpen}
|
onFileOpen={handleFileOpen}
|
||||||
onSelectionChange={setSelection}
|
onSelectionChange={setSelection}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
isLoading={isLoading}
|
|
||||||
isConnected={!!sshSessionId}
|
|
||||||
onPathChange={navigateTo}
|
onPathChange={navigateTo}
|
||||||
onRefresh={handleRefreshDirectory}
|
onRefresh={handleRefreshDirectory}
|
||||||
onUpload={handleFilesDropped}
|
onUpload={handleFilesDropped}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { FileItem } from "@/types/index";
|
import type { FileItem } from "@/types/index";
|
||||||
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
|
|
||||||
|
|
||||||
interface CreateIntent {
|
interface CreateIntent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,8 +65,6 @@ interface FileManagerGridProps {
|
|||||||
onFileOpen: (file: FileItem) => void;
|
onFileOpen: (file: FileItem) => void;
|
||||||
onSelectionChange: (files: FileItem[]) => void;
|
onSelectionChange: (files: FileItem[]) => void;
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
isLoading?: boolean;
|
|
||||||
isConnected?: boolean;
|
|
||||||
onPathChange: (path: string) => void;
|
onPathChange: (path: string) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onUpload?: (files: FileList) => void;
|
onUpload?: (files: FileList) => void;
|
||||||
@@ -191,8 +188,6 @@ export function FileManagerGrid({
|
|||||||
onFileOpen,
|
onFileOpen,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
currentPath,
|
currentPath,
|
||||||
isLoading,
|
|
||||||
isConnected,
|
|
||||||
onPathChange,
|
onPathChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onUpload,
|
onUpload,
|
||||||
@@ -1240,11 +1235,6 @@ export function FileManagerGrid({
|
|||||||
</div>,
|
</div>,
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SimpleLoader
|
|
||||||
visible={!!isLoading && !!isConnected}
|
|
||||||
message={t("common.loading")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
|
|||||||
.then((status) => {
|
.then((status) => {
|
||||||
if (status.guacd.status !== "connected") {
|
if (status.guacd.status !== "connected") {
|
||||||
setError(
|
setError(
|
||||||
"Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible.",
|
"Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible and configured properly in admin settings.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,46 @@ import { ConnectionLog } from "@/ssh/connection-log/ConnectionLog.tsx";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
|
|
||||||
|
// Background/foreground per UI theme for "Termix Default" — must match index.css
|
||||||
|
const TERMIX_DEFAULT_COLORS: Record<
|
||||||
|
string,
|
||||||
|
{ background: string; foreground: string }
|
||||||
|
> = {
|
||||||
|
dark: { background: "#0c0d0b", foreground: "#fafafa" },
|
||||||
|
light: { background: "#ffffff", foreground: "#111210" },
|
||||||
|
dracula: { background: "#282a36", foreground: "#f8f8f2" },
|
||||||
|
catppuccin: { background: "#1e1e2e", foreground: "#cdd6f4" },
|
||||||
|
nord: { background: "#2e3440", foreground: "#eceff4" },
|
||||||
|
solarized: { background: "#002b36", foreground: "#839496" },
|
||||||
|
"tokyo-night": { background: "#1a1b26", foreground: "#a9b1d6" },
|
||||||
|
"one-dark": { background: "#282c34", foreground: "#abb2bf" },
|
||||||
|
gruvbox: { background: "#282828", foreground: "#ebdbb2" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveTermixThemeColors(activeTheme: string, appTheme: string) {
|
||||||
|
if (activeTheme !== "termix") {
|
||||||
|
return (
|
||||||
|
TERMINAL_THEMES[activeTheme]?.colors || TERMINAL_THEMES.termixDark.colors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let resolvedUiTheme = appTheme;
|
||||||
|
if (appTheme === "system") {
|
||||||
|
resolvedUiTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
const uiColors =
|
||||||
|
TERMIX_DEFAULT_COLORS[resolvedUiTheme] ?? TERMIX_DEFAULT_COLORS.dark;
|
||||||
|
const base = TERMINAL_THEMES.termixDark.colors;
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
background: uiColors.background,
|
||||||
|
foreground: uiColors.foreground,
|
||||||
|
cursor: uiColors.foreground,
|
||||||
|
cursorAccent: uiColors.background,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface HostConfig {
|
interface HostConfig {
|
||||||
id?: number;
|
id?: number;
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
@@ -130,27 +170,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
DEFAULT_TERMINAL_CONFIG.theme,
|
DEFAULT_TERMINAL_CONFIG.theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDarkMode =
|
|
||||||
appTheme === "dark" ||
|
|
||||||
appTheme === "dracula" ||
|
|
||||||
appTheme === "gentlemansChoice" ||
|
|
||||||
appTheme === "midnightEspresso" ||
|
|
||||||
appTheme === "catppuccinMocha" ||
|
|
||||||
(appTheme === "system" &&
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
||||||
|
|
||||||
let themeColors;
|
|
||||||
const activeTheme = previewTheme || config.theme;
|
const activeTheme = previewTheme || config.theme;
|
||||||
|
const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
|
||||||
if (activeTheme === "termix") {
|
|
||||||
themeColors = isDarkMode
|
|
||||||
? TERMINAL_THEMES.termixDark.colors
|
|
||||||
: TERMINAL_THEMES.termixLight.colors;
|
|
||||||
} else {
|
|
||||||
themeColors =
|
|
||||||
TERMINAL_THEMES[activeTheme]?.colors ||
|
|
||||||
TERMINAL_THEMES.termixDark.colors;
|
|
||||||
}
|
|
||||||
const backgroundColor = themeColors.background;
|
const backgroundColor = themeColors.background;
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||||
const webSocketRef = useRef<WebSocket | null>(null);
|
const webSocketRef = useRef<WebSocket | null>(null);
|
||||||
@@ -1828,18 +1849,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
...hostConfig.terminalConfig,
|
...hostConfig.terminalConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
let themeColors;
|
|
||||||
const activeTheme = previewTheme || config.theme;
|
const activeTheme = previewTheme || config.theme;
|
||||||
|
const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
|
||||||
if (activeTheme === "termix") {
|
|
||||||
themeColors = isDarkMode
|
|
||||||
? TERMINAL_THEMES.termixDark.colors
|
|
||||||
: TERMINAL_THEMES.termixLight.colors;
|
|
||||||
} else {
|
|
||||||
themeColors =
|
|
||||||
TERMINAL_THEMES[activeTheme]?.colors ||
|
|
||||||
TERMINAL_THEMES.termixDark.colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontConfig = TERMINAL_FONTS.find(
|
const fontConfig = TERMINAL_FONTS.find(
|
||||||
(f) => f.value === config.fontFamily,
|
(f) => f.value === config.fontFamily,
|
||||||
@@ -1896,13 +1907,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
|
|
||||||
// Refresh terminal to apply new theme colors to existing buffer content
|
// Refresh terminal to apply new theme colors to existing buffer content
|
||||||
hardRefresh();
|
hardRefresh();
|
||||||
}, [
|
}, [terminal, hostConfig.terminalConfig, previewTheme, appTheme, isFitted]);
|
||||||
terminal,
|
|
||||||
hostConfig.terminalConfig,
|
|
||||||
previewTheme,
|
|
||||||
isDarkMode,
|
|
||||||
isFitted,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminal || !xtermRef.current) return;
|
if (!terminal || !xtermRef.current) return;
|
||||||
@@ -1917,18 +1922,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
);
|
);
|
||||||
const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback;
|
const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback;
|
||||||
|
|
||||||
let themeColors;
|
|
||||||
const activeTheme = previewTheme || config.theme;
|
const activeTheme = previewTheme || config.theme;
|
||||||
|
const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
|
||||||
if (activeTheme === "termix") {
|
|
||||||
themeColors = isDarkMode
|
|
||||||
? TERMINAL_THEMES.termixDark.colors
|
|
||||||
: TERMINAL_THEMES.termixLight.colors;
|
|
||||||
} else {
|
|
||||||
themeColors =
|
|
||||||
TERMINAL_THEMES[activeTheme]?.colors ||
|
|
||||||
TERMINAL_THEMES.termixDark.colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial options before opening the terminal
|
// Set initial options before opening the terminal
|
||||||
terminal.options = {
|
terminal.options = {
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"authentication": "Authentication",
|
"authentication": "Authentication",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"passwordSaved": "Password saved, type to change",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"credential": "Credential",
|
"credential": "Credential",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
@@ -435,11 +436,24 @@
|
|||||||
"copiedToClipboard": "Copied to clipboard",
|
"copiedToClipboard": "Copied to clipboard",
|
||||||
"terminalUrlCopied": "Terminal URL copied",
|
"terminalUrlCopied": "Terminal URL copied",
|
||||||
"fileManagerUrlCopied": "File Manager URL copied",
|
"fileManagerUrlCopied": "File Manager URL copied",
|
||||||
|
"tunnelUrlCopied": "Tunnel URL copied",
|
||||||
|
"dockerUrlCopied": "Docker URL copied",
|
||||||
|
"serverStatsUrlCopied": "Server Stats URL copied",
|
||||||
|
"rdpUrlCopied": "RDP URL copied",
|
||||||
|
"vncUrlCopied": "VNC URL copied",
|
||||||
|
"telnetUrlCopied": "Telnet URL copied",
|
||||||
"remoteDesktopUrlCopied": "Remote Desktop URL copied",
|
"remoteDesktopUrlCopied": "Remote Desktop URL copied",
|
||||||
"cloneHostAction": "Clone Host",
|
"cloneHostAction": "Clone Host",
|
||||||
"copyAddress": "Copy Address",
|
"copyAddress": "Copy Address",
|
||||||
|
"copyLink": "Copy Link",
|
||||||
"copyTerminalUrlAction": "Copy Terminal URL",
|
"copyTerminalUrlAction": "Copy Terminal URL",
|
||||||
"copyFileManagerUrlAction": "Copy File Manager URL",
|
"copyFileManagerUrlAction": "Copy File Manager URL",
|
||||||
|
"copyTunnelUrlAction": "Copy Tunnel URL",
|
||||||
|
"copyDockerUrlAction": "Copy Docker URL",
|
||||||
|
"copyServerStatsUrlAction": "Copy Server Stats URL",
|
||||||
|
"copyRdpUrlAction": "Copy RDP URL",
|
||||||
|
"copyVncUrlAction": "Copy VNC URL",
|
||||||
|
"copyTelnetUrlAction": "Copy Telnet URL",
|
||||||
"copyRemoteDesktopUrlAction": "Copy Remote Desktop URL",
|
"copyRemoteDesktopUrlAction": "Copy Remote Desktop URL",
|
||||||
"deleteCredentialConfirm": "Delete credential \"{{name}}\"?",
|
"deleteCredentialConfirm": "Delete credential \"{{name}}\"?",
|
||||||
"deletedCredential": "Deleted {{name}}",
|
"deletedCredential": "Deleted {{name}}",
|
||||||
|
|||||||
+6
-173
@@ -1107,102 +1107,18 @@ export async function getSSHHosts(): Promise<SSHHostWithStatus[]> {
|
|||||||
|
|
||||||
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
|
||||||
connectionType: hostData.connectionType || "ssh",
|
|
||||||
name: hostData.name || "",
|
|
||||||
ip: hostData.ip,
|
|
||||||
port: parseInt(hostData.port.toString()) || 22,
|
|
||||||
username: hostData.username,
|
|
||||||
folder: hostData.folder || "",
|
|
||||||
tags: hostData.tags || [],
|
|
||||||
pin: Boolean(hostData.pin),
|
|
||||||
authType: hostData.authType,
|
|
||||||
password:
|
|
||||||
hostData.connectionType !== "ssh"
|
|
||||||
? hostData.password || null
|
|
||||||
: hostData.authType === "password"
|
|
||||||
? hostData.password
|
|
||||||
: null,
|
|
||||||
key: hostData.authType === "key" ? hostData.key : null,
|
|
||||||
keyPassword: hostData.authType === "key" ? hostData.keyPassword : null,
|
|
||||||
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
|
||||||
credentialId:
|
|
||||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
|
||||||
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
|
||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
|
||||||
enableDocker: Boolean(hostData.enableDocker),
|
|
||||||
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
|
|
||||||
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
|
|
||||||
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
|
|
||||||
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
|
|
||||||
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
|
|
||||||
defaultPath: hostData.defaultPath || "/",
|
|
||||||
tunnelConnections: hostData.tunnelConnections || [],
|
|
||||||
jumpHosts: hostData.jumpHosts || [],
|
|
||||||
quickActions: hostData.quickActions || [],
|
|
||||||
sudoPassword: hostData.sudoPassword || null,
|
|
||||||
statsConfig: hostData.statsConfig || null,
|
|
||||||
dockerConfig: hostData.dockerConfig || null,
|
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
|
||||||
domain: hostData.domain || null,
|
|
||||||
security: hostData.security || null,
|
|
||||||
ignoreCert: Boolean(hostData.ignoreCert),
|
|
||||||
guacamoleConfig: hostData.guacamoleConfig || null,
|
|
||||||
notes: hostData.notes || "",
|
|
||||||
useSocks5: Boolean(hostData.useSocks5),
|
|
||||||
socks5Host: hostData.socks5Host || null,
|
|
||||||
socks5Port: hostData.socks5Port || null,
|
|
||||||
socks5Username: hostData.socks5Username || null,
|
|
||||||
socks5Password: hostData.socks5Password || null,
|
|
||||||
socks5ProxyChain: hostData.socks5ProxyChain || null,
|
|
||||||
macAddress: hostData.macAddress || null,
|
|
||||||
portKnockSequence: hostData.portKnockSequence || null,
|
|
||||||
enableSsh: hostData.enableSsh !== false,
|
|
||||||
enableRdp: Boolean(hostData.enableRdp),
|
|
||||||
enableVnc: Boolean(hostData.enableVnc),
|
|
||||||
enableTelnet: Boolean(hostData.enableTelnet),
|
|
||||||
sshPort: hostData.sshPort || hostData.port || 22,
|
|
||||||
rdpPort: hostData.rdpPort || 3389,
|
|
||||||
vncPort: hostData.vncPort || 5900,
|
|
||||||
telnetPort: hostData.telnetPort || 23,
|
|
||||||
rdpUser: hostData.rdpUser || null,
|
|
||||||
rdpPassword: hostData.rdpPassword || null,
|
|
||||||
rdpDomain: hostData.rdpDomain || null,
|
|
||||||
rdpSecurity: hostData.rdpSecurity || null,
|
|
||||||
rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert),
|
|
||||||
vncPassword: hostData.vncPassword || null,
|
|
||||||
vncUser: hostData.vncUser || null,
|
|
||||||
telnetUser: hostData.telnetUser || null,
|
|
||||||
telnetPassword: hostData.telnetPassword || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
|
||||||
submitData.tunnelConnections = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!submitData.enableFileManager) {
|
|
||||||
submitData.defaultPath = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostData.authType === "key" && hostData.key instanceof File) {
|
if (hostData.authType === "key" && hostData.key instanceof File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("key", hostData.key);
|
formData.append("key", hostData.key);
|
||||||
|
const dataWithoutFile = { ...hostData, key: undefined };
|
||||||
const dataWithoutFile = { ...submitData };
|
|
||||||
delete dataWithoutFile.key;
|
|
||||||
formData.append("data", JSON.stringify(dataWithoutFile));
|
formData.append("data", JSON.stringify(dataWithoutFile));
|
||||||
|
|
||||||
const response = await sshHostApi.post("/db/host", formData, {
|
const response = await sshHostApi.post("/db/host", formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
|
||||||
const response = await sshHostApi.post("/db/host", submitData);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
const response = await sshHostApi.post("/db/host", hostData);
|
||||||
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleApiError(error, "create SSH host");
|
throw handleApiError(error, "create SSH host");
|
||||||
}
|
}
|
||||||
@@ -1213,101 +1129,18 @@ export async function updateSSHHost(
|
|||||||
hostData: SSHHostData,
|
hostData: SSHHostData,
|
||||||
): Promise<SSHHost> {
|
): Promise<SSHHost> {
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
|
||||||
connectionType: hostData.connectionType || "ssh",
|
|
||||||
name: hostData.name || "",
|
|
||||||
ip: hostData.ip,
|
|
||||||
port: parseInt(hostData.port.toString()) || 22,
|
|
||||||
username: hostData.username,
|
|
||||||
folder: hostData.folder || "",
|
|
||||||
tags: hostData.tags || [],
|
|
||||||
pin: Boolean(hostData.pin),
|
|
||||||
authType: hostData.authType,
|
|
||||||
password:
|
|
||||||
hostData.connectionType !== "ssh"
|
|
||||||
? hostData.password || null
|
|
||||||
: hostData.authType === "password"
|
|
||||||
? hostData.password
|
|
||||||
: null,
|
|
||||||
key: hostData.authType === "key" ? hostData.key : null,
|
|
||||||
keyPassword: hostData.authType === "key" ? hostData.keyPassword : null,
|
|
||||||
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
|
||||||
credentialId:
|
|
||||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
|
||||||
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
|
||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
|
||||||
enableDocker: Boolean(hostData.enableDocker),
|
|
||||||
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
|
|
||||||
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
|
|
||||||
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
|
|
||||||
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
|
|
||||||
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
|
|
||||||
defaultPath: hostData.defaultPath || "/",
|
|
||||||
tunnelConnections: hostData.tunnelConnections || [],
|
|
||||||
jumpHosts: hostData.jumpHosts || [],
|
|
||||||
quickActions: hostData.quickActions || [],
|
|
||||||
sudoPassword: hostData.sudoPassword || null,
|
|
||||||
statsConfig: hostData.statsConfig || null,
|
|
||||||
dockerConfig: hostData.dockerConfig || null,
|
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
|
||||||
domain: hostData.domain || null,
|
|
||||||
security: hostData.security || null,
|
|
||||||
ignoreCert: Boolean(hostData.ignoreCert),
|
|
||||||
guacamoleConfig: hostData.guacamoleConfig || null,
|
|
||||||
notes: hostData.notes || "",
|
|
||||||
useSocks5: Boolean(hostData.useSocks5),
|
|
||||||
socks5Host: hostData.socks5Host || null,
|
|
||||||
socks5Port: hostData.socks5Port || null,
|
|
||||||
socks5Username: hostData.socks5Username || null,
|
|
||||||
socks5Password: hostData.socks5Password || null,
|
|
||||||
socks5ProxyChain: hostData.socks5ProxyChain || null,
|
|
||||||
macAddress: hostData.macAddress || null,
|
|
||||||
portKnockSequence: hostData.portKnockSequence || null,
|
|
||||||
enableSsh: hostData.enableSsh !== false,
|
|
||||||
enableRdp: Boolean(hostData.enableRdp),
|
|
||||||
enableVnc: Boolean(hostData.enableVnc),
|
|
||||||
enableTelnet: Boolean(hostData.enableTelnet),
|
|
||||||
sshPort: hostData.sshPort || hostData.port || 22,
|
|
||||||
rdpPort: hostData.rdpPort || 3389,
|
|
||||||
vncPort: hostData.vncPort || 5900,
|
|
||||||
telnetPort: hostData.telnetPort || 23,
|
|
||||||
rdpUser: hostData.rdpUser || null,
|
|
||||||
rdpPassword: hostData.rdpPassword || null,
|
|
||||||
rdpDomain: hostData.rdpDomain || null,
|
|
||||||
rdpSecurity: hostData.rdpSecurity || null,
|
|
||||||
rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert),
|
|
||||||
vncPassword: hostData.vncPassword || null,
|
|
||||||
vncUser: hostData.vncUser || null,
|
|
||||||
telnetUser: hostData.telnetUser || null,
|
|
||||||
telnetPassword: hostData.telnetPassword || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
|
||||||
submitData.tunnelConnections = [];
|
|
||||||
}
|
|
||||||
if (!submitData.enableFileManager) {
|
|
||||||
submitData.defaultPath = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostData.authType === "key" && hostData.key instanceof File) {
|
if (hostData.authType === "key" && hostData.key instanceof File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("key", hostData.key);
|
formData.append("key", hostData.key);
|
||||||
|
const dataWithoutFile = { ...hostData, key: undefined };
|
||||||
const dataWithoutFile = { ...submitData };
|
|
||||||
delete dataWithoutFile.key;
|
|
||||||
formData.append("data", JSON.stringify(dataWithoutFile));
|
formData.append("data", JSON.stringify(dataWithoutFile));
|
||||||
|
|
||||||
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
|
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
|
||||||
const response = await sshHostApi.put(`/db/host/${hostId}`, submitData);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
const response = await sshHostApi.put(`/db/host/${hostId}`, hostData);
|
||||||
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleApiError(error, "update SSH host");
|
throw handleApiError(error, "update SSH host");
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-14
@@ -23,6 +23,7 @@ import { TunnelTab } from "@/features/tunnel/TunnelTab";
|
|||||||
import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard";
|
import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard";
|
||||||
import type { Tab, TabType, Host } from "@/types/ui-types";
|
import type { Tab, TabType, Host } from "@/types/ui-types";
|
||||||
import type { SSHHost } from "@/types";
|
import type { SSHHost } from "@/types";
|
||||||
|
import { useTabsSafe } from "@/shell/TabContext";
|
||||||
|
|
||||||
function hostToSSHHost(h: Host): SSHHost {
|
function hostToSSHHost(h: Host): SSHHost {
|
||||||
return {
|
return {
|
||||||
@@ -109,6 +110,41 @@ export function tabIcon(type: TabType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TerminalTabContent({
|
||||||
|
tab,
|
||||||
|
host,
|
||||||
|
label,
|
||||||
|
isVisible,
|
||||||
|
onCloseTab,
|
||||||
|
}: {
|
||||||
|
tab: Tab;
|
||||||
|
host: Host;
|
||||||
|
label: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
onCloseTab?: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const { previewTerminalTheme } = useTabsSafe();
|
||||||
|
return (
|
||||||
|
<CommandHistoryProvider>
|
||||||
|
<TerminalFeature
|
||||||
|
ref={tab.terminalRef as any}
|
||||||
|
hostConfig={
|
||||||
|
{
|
||||||
|
...hostToSSHHost(host),
|
||||||
|
sshPort: host.sshPort ?? host.port,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
isVisible={isVisible}
|
||||||
|
title={label}
|
||||||
|
showTitle={false}
|
||||||
|
splitScreen={false}
|
||||||
|
onClose={() => onCloseTab?.(tab.id)}
|
||||||
|
previewTheme={previewTerminalTheme}
|
||||||
|
/>
|
||||||
|
</CommandHistoryProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function renderTabContent(
|
export function renderTabContent(
|
||||||
tab: Tab,
|
tab: Tab,
|
||||||
onOpenSingletonTab?: (type: TabType) => void,
|
onOpenSingletonTab?: (type: TabType) => void,
|
||||||
@@ -136,22 +172,13 @@ export function renderTabContent(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<CommandHistoryProvider>
|
<TerminalTabContent
|
||||||
<TerminalFeature
|
tab={tab}
|
||||||
ref={tab.terminalRef as any}
|
host={host}
|
||||||
hostConfig={
|
label={label}
|
||||||
{
|
|
||||||
...hostToSSHHost(host),
|
|
||||||
sshPort: host.sshPort ?? host.port,
|
|
||||||
} as any
|
|
||||||
}
|
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
title={label}
|
onCloseTab={onCloseTab}
|
||||||
showTitle={false}
|
|
||||||
splitScreen={false}
|
|
||||||
onClose={() => onCloseTab?.(tab.id)}
|
|
||||||
/>
|
/>
|
||||||
</CommandHistoryProvider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case "files":
|
case "files":
|
||||||
|
|||||||
+162
-63
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/lib/terminal-themes";
|
} from "@/lib/terminal-themes";
|
||||||
import { Button } from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import { Input } from "@/components/input";
|
import { Input } from "@/components/input";
|
||||||
|
import { PasswordInput } from "@/components/password-input";
|
||||||
import { Slider } from "@/components/slider";
|
import { Slider } from "@/components/slider";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
|
Link,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Lock,
|
Lock,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
@@ -60,6 +62,9 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/dropdown-menu";
|
} from "@/components/dropdown-menu";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -98,6 +103,7 @@ import {
|
|||||||
import type { SSHHostWithStatus } from "@/main-axios";
|
import type { SSHHostWithStatus } from "@/main-axios";
|
||||||
|
|
||||||
import type { Host, Credential } from "@/types/ui-types";
|
import type { Host, Credential } from "@/types/ui-types";
|
||||||
|
import { useTabsSafe } from "@/shell/TabContext";
|
||||||
|
|
||||||
function sshHostToHost(h: SSHHostWithStatus): Host {
|
function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||||
const parseJson = (v: any) => {
|
const parseJson = (v: any) => {
|
||||||
@@ -125,6 +131,8 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
|||||||
tags: h.tags ?? [],
|
tags: h.tags ?? [],
|
||||||
authType: h.authType,
|
authType: h.authType,
|
||||||
password: h.password,
|
password: h.password,
|
||||||
|
hasPassword: !!(h as any).hasPassword || !!h.password,
|
||||||
|
hasKey: !!(h as any).hasKey || !!(typeof h.key === "string" && h.key),
|
||||||
key: typeof h.key === "string" ? h.key : undefined,
|
key: typeof h.key === "string" ? h.key : undefined,
|
||||||
keyPassword: h.keyPassword,
|
keyPassword: h.keyPassword,
|
||||||
keyType: h.keyType,
|
keyType: h.keyType,
|
||||||
@@ -132,15 +140,10 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
|||||||
notes: h.notes,
|
notes: h.notes,
|
||||||
pin: h.pin ?? false,
|
pin: h.pin ?? false,
|
||||||
macAddress: h.macAddress,
|
macAddress: h.macAddress,
|
||||||
enableSsh:
|
enableSsh: h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh",
|
||||||
h.enableSsh != null
|
|
||||||
? h.enableSsh
|
|
||||||
: h.connectionType === "ssh" || !h.connectionType,
|
|
||||||
enableTerminal:
|
enableTerminal:
|
||||||
h.enableTerminal ??
|
h.enableTerminal ??
|
||||||
(h.enableSsh != null
|
(h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh"),
|
||||||
? h.enableSsh
|
|
||||||
: h.connectionType === "ssh" || !h.connectionType),
|
|
||||||
enableTunnel: h.enableTunnel ?? false,
|
enableTunnel: h.enableTunnel ?? false,
|
||||||
enableFileManager: h.enableFileManager ?? false,
|
enableFileManager: h.enableFileManager ?? false,
|
||||||
enableDocker: h.enableDocker ?? false,
|
enableDocker: h.enableDocker ?? false,
|
||||||
@@ -539,7 +542,7 @@ function HostRow({
|
|||||||
paddingRight: "8px",
|
paddingRight: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{host.enableTerminal && (
|
{host.enableSsh && host.enableTerminal && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -551,7 +554,7 @@ function HostRow({
|
|||||||
<span>{t("hosts.terminal")}</span>
|
<span>{t("hosts.terminal")}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{host.enableFileManager && (
|
{host.enableSsh && host.enableFileManager && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -563,7 +566,7 @@ function HostRow({
|
|||||||
<span>{t("hosts.fileManager")}</span>
|
<span>{t("hosts.fileManager")}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{host.enableDocker && (
|
{host.enableSsh && host.enableDocker && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -575,7 +578,7 @@ function HostRow({
|
|||||||
<span>{t("hosts.docker")}</span>
|
<span>{t("hosts.docker")}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{host.enableTunnel && (
|
{host.enableSsh && host.enableTunnel && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -587,7 +590,7 @@ function HostRow({
|
|||||||
<span>{t("hosts.tunnel")}</span>
|
<span>{t("hosts.tunnel")}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{metricsEnabled && (
|
{host.enableSsh && metricsEnabled && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -672,7 +675,13 @@ function HostRow({
|
|||||||
<Copy className="size-3.5 mr-2" />
|
<Copy className="size-3.5 mr-2" />
|
||||||
{t("hosts.copyAddress")}
|
{t("hosts.copyAddress")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{host.enableTerminal && (
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Link className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyLink")}
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{host.enableSsh && host.enableTerminal && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
@@ -681,41 +690,103 @@ function HostRow({
|
|||||||
toast.success(t("hosts.terminalUrlCopied"));
|
toast.success(t("hosts.terminalUrlCopied"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy className="size-3.5 mr-2" />
|
<Terminal className="size-3.5 mr-2" />
|
||||||
{t("hosts.copyTerminalUrlAction")}
|
{t("hosts.copyTerminalUrlAction")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{host.enableFileManager && (
|
{host.enableSsh && host.enableFileManager && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`${window.location.origin}?view=file_manager&hostId=${host.id}`,
|
`${window.location.origin}?view=file-manager&hostId=${host.id}`,
|
||||||
);
|
);
|
||||||
toast.success(t("hosts.fileManagerUrlCopied"));
|
toast.success(t("hosts.fileManagerUrlCopied"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy className="size-3.5 mr-2" />
|
<FolderSearch className="size-3.5 mr-2" />
|
||||||
{t("hosts.copyFileManagerUrlAction")}
|
{t("hosts.copyFileManagerUrlAction")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(host.enableRdp || host.enableVnc || host.enableTelnet) && (
|
{host.enableSsh && host.enableTunnel && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const proto = host.enableRdp
|
|
||||||
? "rdp"
|
|
||||||
: host.enableVnc
|
|
||||||
? "vnc"
|
|
||||||
: "telnet";
|
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`${window.location.origin}?view=${proto}&hostId=${host.id}`,
|
`${window.location.origin}?view=tunnel&hostId=${host.id}`,
|
||||||
);
|
);
|
||||||
toast.success(t("hosts.remoteDesktopUrlCopied"));
|
toast.success(t("hosts.tunnelUrlCopied"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy className="size-3.5 mr-2" />
|
<Network className="size-3.5 mr-2" />
|
||||||
{t("hosts.copyRemoteDesktopUrlAction")}
|
{t("hosts.copyTunnelUrlAction")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{host.enableSsh && host.enableDocker && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}?view=docker&hostId=${host.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("hosts.dockerUrlCopied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyDockerUrlAction")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableSsh && metricsEnabled && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}?view=server-stats&hostId=${host.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("hosts.serverStatsUrlCopied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Server className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyServerStatsUrlAction")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableRdp && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}?view=rdp&hostId=${host.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("hosts.rdpUrlCopied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Monitor className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyRdpUrlAction")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableVnc && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}?view=vnc&hostId=${host.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("hosts.vncUrlCopied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Monitor className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyVncUrlAction")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableTelnet && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}?view=telnet&hostId=${host.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("hosts.telnetUrlCopied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Terminal className="size-3.5 mr-2" />
|
||||||
|
{t("hosts.copyTelnetUrlAction")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
@@ -759,6 +830,7 @@ function HostEditor({
|
|||||||
credentials: { id: string; name: string; username: string }[];
|
credentials: { id: string; name: string; username: string }[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setPreviewTerminalTheme } = useTabsSafe();
|
||||||
const [form, setForm] = useState(() => {
|
const [form, setForm] = useState(() => {
|
||||||
const rawTheme = host?.terminalConfig?.theme;
|
const rawTheme = host?.terminalConfig?.theme;
|
||||||
const normalizedTheme =
|
const normalizedTheme =
|
||||||
@@ -779,8 +851,9 @@ function HostEditor({
|
|||||||
vncPort: host?.vncPort ?? 5900,
|
vncPort: host?.vncPort ?? 5900,
|
||||||
telnetPort: host?.telnetPort ?? 23,
|
telnetPort: host?.telnetPort ?? 23,
|
||||||
authType: host?.authType ?? "password",
|
authType: host?.authType ?? "password",
|
||||||
password: host?.password ?? "",
|
password:
|
||||||
key: host?.key ?? "",
|
host?.password ?? (host?.hasPassword ? "existing_password" : ""),
|
||||||
|
key: host?.key ?? (host?.hasKey ? "existing_key" : ""),
|
||||||
keyPassword: host?.keyPassword ?? "",
|
keyPassword: host?.keyPassword ?? "",
|
||||||
keyType: host?.keyType ?? "auto",
|
keyType: host?.keyType ?? "auto",
|
||||||
keySubTab: "paste" as "paste" | "upload",
|
keySubTab: "paste" as "paste" | "upload",
|
||||||
@@ -994,8 +1067,11 @@ function HostEditor({
|
|||||||
tags,
|
tags,
|
||||||
pin: form.pin,
|
pin: form.pin,
|
||||||
authType: form.authType,
|
authType: form.authType,
|
||||||
password: form.password || null,
|
password:
|
||||||
key: form.key || null,
|
form.password === "existing_password"
|
||||||
|
? undefined
|
||||||
|
: form.password || null,
|
||||||
|
key: form.key === "existing_key" ? undefined : form.key || null,
|
||||||
keyPassword: form.keyPassword || null,
|
keyPassword: form.keyPassword || null,
|
||||||
keyType: form.keyType !== "auto" ? form.keyType : null,
|
keyType: form.keyType !== "auto" ? form.keyType : null,
|
||||||
credentialId: form.credentialId ? Number(form.credentialId) : null,
|
credentialId: form.credentialId ? Number(form.credentialId) : null,
|
||||||
@@ -1088,6 +1164,7 @@ function HostEditor({
|
|||||||
? await updateSSHHost(Number(host.id), data as any)
|
? await updateSSHHost(Number(host.id), data as any)
|
||||||
: await createSSHHost(data as any);
|
: await createSSHHost(data as any);
|
||||||
toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated"));
|
toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated"));
|
||||||
|
setPreviewTerminalTheme(null);
|
||||||
onSave(saved);
|
onSave(saved);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t("hosts.failedToSave"));
|
toast.error(t("hosts.failedToSave"));
|
||||||
@@ -1515,9 +1592,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.proxyPassword")}
|
{t("hosts.proxyPassword")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs pr-8"
|
||||||
type="password"
|
|
||||||
placeholder={t("hosts.optional")}
|
placeholder={t("hosts.optional")}
|
||||||
value={form.socks5Password}
|
value={form.socks5Password}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -1629,9 +1705,8 @@ function HostEditor({
|
|||||||
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.proxyPassword")}
|
{t("hosts.proxyPassword")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs pr-8"
|
||||||
type="password"
|
|
||||||
placeholder={t("hosts.optional")}
|
placeholder={t("hosts.optional")}
|
||||||
value={node.password}
|
value={node.password}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1854,10 +1929,22 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.password")}
|
{t("hosts.password")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder={
|
||||||
value={form.password}
|
form.password === "existing_password"
|
||||||
|
? t("hosts.passwordSaved")
|
||||||
|
: "••••••••"
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
form.password === "existing_password"
|
||||||
|
? ""
|
||||||
|
: form.password
|
||||||
|
}
|
||||||
|
onFocus={() => {
|
||||||
|
if (form.password === "existing_password")
|
||||||
|
setField("password", "");
|
||||||
|
}}
|
||||||
onChange={(e) => setField("password", e.target.value)}
|
onChange={(e) => setField("password", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1932,9 +2019,9 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.keyPassphrase")}
|
{t("hosts.keyPassphrase")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="Optional"
|
placeholder={t("hosts.optional")}
|
||||||
value={form.keyPassword}
|
value={form.keyPassword}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setField("keyPassword", e.target.value)
|
setField("keyPassword", e.target.value)
|
||||||
@@ -2065,7 +2152,10 @@ function HostEditor({
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={form.theme}
|
value={form.theme}
|
||||||
onChange={(e) => setField("theme", e.target.value)}
|
onChange={(e) => {
|
||||||
|
setField("theme", e.target.value);
|
||||||
|
setPreviewTerminalTheme(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"
|
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"
|
||||||
>
|
>
|
||||||
{Object.entries(TERMINAL_THEMES)
|
{Object.entries(TERMINAL_THEMES)
|
||||||
@@ -2286,8 +2376,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.sudoPasswordLabel")}
|
{t("hosts.sudoPasswordLabel")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={form.sudoPassword}
|
value={form.sudoPassword}
|
||||||
onChange={(e) => setField("sudoPassword", e.target.value)}
|
onChange={(e) => setField("sudoPassword", e.target.value)}
|
||||||
@@ -3178,8 +3268,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.guac.password")}
|
{t("hosts.guac.password")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={form.rdpPassword}
|
value={form.rdpPassword}
|
||||||
onChange={(e) => setField("rdpPassword", e.target.value)}
|
onChange={(e) => setField("rdpPassword", e.target.value)}
|
||||||
@@ -3665,8 +3755,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.guac.gatewayPassword")}
|
{t("hosts.guac.gatewayPassword")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={form.guacamoleConfig["gateway-password"] ?? ""}
|
value={form.guacamoleConfig["gateway-password"] ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -3954,8 +4044,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.guac.vncPassword")}
|
{t("hosts.guac.vncPassword")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={form.vncPassword}
|
value={form.vncPassword}
|
||||||
onChange={(e) => setField("vncPassword", e.target.value)}
|
onChange={(e) => setField("vncPassword", e.target.value)}
|
||||||
@@ -4338,8 +4428,8 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.guac.password")}
|
{t("hosts.guac.password")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={form.telnetPassword}
|
value={form.telnetPassword}
|
||||||
onChange={(e) => setField("telnetPassword", e.target.value)}
|
onChange={(e) => setField("telnetPassword", e.target.value)}
|
||||||
@@ -4753,7 +4843,14 @@ function HostEditor({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 mt-3 mb-6">
|
<div className="flex justify-end gap-3 mt-3 mb-6">
|
||||||
<Button variant="ghost" onClick={onBack} disabled={saving}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewTerminalTheme(null);
|
||||||
|
onBack();
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
{t("hosts.guac.cancelBtn")}
|
{t("hosts.guac.cancelBtn")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4975,8 +5072,8 @@ function CredentialEditorView({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.password")}
|
{t("hosts.password")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={credForm.value}
|
value={credForm.value}
|
||||||
onChange={(e) => setCredField("value", e.target.value)}
|
onChange={(e) => setCredField("value", e.target.value)}
|
||||||
@@ -5083,8 +5180,8 @@ function CredentialEditorView({
|
|||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("hosts.keyPassphraseOptional")}
|
{t("hosts.keyPassphraseOptional")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
className="h-8 text-xs pr-8"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={credForm.passphrase}
|
value={credForm.passphrase}
|
||||||
onChange={(e) => setCredField("passphrase", e.target.value)}
|
onChange={(e) => setCredField("passphrase", e.target.value)}
|
||||||
@@ -6033,6 +6130,7 @@ export function HostManager({
|
|||||||
}
|
}
|
||||||
return [...prev, updated];
|
return [...prev, updated];
|
||||||
});
|
});
|
||||||
|
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
|
||||||
setEditingHost(null);
|
setEditingHost(null);
|
||||||
setActiveHostTab("general");
|
setActiveHostTab("general");
|
||||||
}}
|
}}
|
||||||
@@ -6173,9 +6271,7 @@ export function HostManager({
|
|||||||
const normalized = hostsArray.map((h: any) => ({
|
const normalized = hostsArray.map((h: any) => ({
|
||||||
...h,
|
...h,
|
||||||
port: h.port ?? h.sshPort ?? 22,
|
port: h.port ?? h.sshPort ?? 22,
|
||||||
enableSsh:
|
enableSsh: h.enableSsh ?? h.connectionType === "ssh",
|
||||||
h.enableSsh ??
|
|
||||||
(h.connectionType === "ssh" || !h.connectionType),
|
|
||||||
enableRdp: h.enableRdp ?? h.connectionType === "rdp",
|
enableRdp: h.enableRdp ?? h.connectionType === "rdp",
|
||||||
enableVnc: h.enableVnc ?? h.connectionType === "vnc",
|
enableVnc: h.enableVnc ?? h.connectionType === "vnc",
|
||||||
enableTelnet:
|
enableTelnet:
|
||||||
@@ -6187,6 +6283,9 @@ export function HostManager({
|
|||||||
);
|
);
|
||||||
const raw = await getSSHHosts();
|
const raw = await getSSHHosts();
|
||||||
setHosts(raw.map(sshHostToHost));
|
setHosts(raw.map(sshHostToHost));
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("termix:hosts-changed"),
|
||||||
|
);
|
||||||
const msg = [
|
const msg = [
|
||||||
result.success ? `${result.success} imported` : null,
|
result.success ? `${result.success} imported` : null,
|
||||||
result.updated ? `${result.updated} updated` : null,
|
result.updated ? `${result.updated} updated` : null,
|
||||||
|
|||||||
@@ -21,23 +21,28 @@ export function isFolder(item: Host | HostFolder): item is HostFolder {
|
|||||||
function getSshActions(
|
function getSshActions(
|
||||||
host: Host,
|
host: Host,
|
||||||
): { type: TabType; icon: typeof Terminal; label: string }[] {
|
): { type: TabType; icon: typeof Terminal; label: string }[] {
|
||||||
const metricsEnabled = host.statsConfig?.metricsEnabled !== false;
|
const metricsEnabled =
|
||||||
|
host.enableSsh && host.statsConfig?.metricsEnabled !== false;
|
||||||
return [
|
return [
|
||||||
|
host.enableSsh &&
|
||||||
host.enableTerminal && {
|
host.enableTerminal && {
|
||||||
type: "terminal" as TabType,
|
type: "terminal" as TabType,
|
||||||
icon: Terminal,
|
icon: Terminal,
|
||||||
label: "Terminal",
|
label: "Terminal",
|
||||||
},
|
},
|
||||||
|
host.enableSsh &&
|
||||||
host.enableFileManager && {
|
host.enableFileManager && {
|
||||||
type: "files" as TabType,
|
type: "files" as TabType,
|
||||||
icon: FolderSearch,
|
icon: FolderSearch,
|
||||||
label: "Files",
|
label: "Files",
|
||||||
},
|
},
|
||||||
|
host.enableSsh &&
|
||||||
host.enableDocker && {
|
host.enableDocker && {
|
||||||
type: "docker" as TabType,
|
type: "docker" as TabType,
|
||||||
icon: Box,
|
icon: Box,
|
||||||
label: "Docker",
|
label: "Docker",
|
||||||
},
|
},
|
||||||
|
host.enableSsh &&
|
||||||
host.enableTunnel && {
|
host.enableTunnel && {
|
||||||
type: "tunnel" as TabType,
|
type: "tunnel" as TabType,
|
||||||
icon: Network,
|
icon: Network,
|
||||||
@@ -211,8 +216,7 @@ export function HostItem({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center flex-wrap gap-1 pt-1.5 pl-2 pb-1">
|
<div className="flex items-center flex-wrap gap-1 pt-1.5 pl-2 pb-1">
|
||||||
{host.enableSsh &&
|
{getSshActions(host).map(({ type, icon: Icon, label }) => (
|
||||||
getSshActions(host).map(({ type, icon: Icon, label }) => (
|
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
title={label}
|
title={label}
|
||||||
@@ -226,7 +230,8 @@ export function HostItem({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{host.enableSsh &&
|
{host.enableSsh &&
|
||||||
(host.enableRdp || host.enableVnc || host.enableTelnet) && (
|
(host.enableRdp || host.enableVnc || host.enableTelnet) &&
|
||||||
|
getSshActions(host).length > 0 && (
|
||||||
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
|
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
|
||||||
)}
|
)}
|
||||||
{host.enableRdp && (
|
{host.enableRdp && (
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ export function WarpgateDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 flex items-center justify-center z-500 animate-in fade-in duration-200">
|
<div className="absolute inset-0 flex items-center justify-center z-500 animate-in fade-in duration-200 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-canvas rounded-md"
|
className="absolute inset-0 bg-canvas rounded-md"
|
||||||
style={{ backgroundColor: backgroundColor || undefined }}
|
style={{ backgroundColor: backgroundColor || undefined }}
|
||||||
/>
|
/>
|
||||||
<div className="bg-card border border-border w-full max-w-md mx-4 relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
<div className="bg-card border border-border w-full max-w-md mx-4 my-4 relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
||||||
<div className="p-4 border-b border-border">
|
<div className="p-4 border-b border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="size-4 text-accent-brand" />
|
<Shield className="size-4 text-accent-brand" />
|
||||||
@@ -95,7 +95,7 @@ export function WarpgateDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-end gap-2 pt-1">
|
<div className="flex flex-wrap justify-end gap-2 pt-1">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
Reference in New Issue
Block a user