feat: host manager improvements and added over electron files

This commit is contained in:
LukeGus
2026-05-20 13:58:15 -05:00
parent d3fa8c4420
commit ec69c338da
12 changed files with 535 additions and 350 deletions
+3
View File
@@ -143,6 +143,9 @@ function App() {
setAuthUsername(u);
setPhase("fading-in");
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
if (isElectron()) {
window.electronAPI?.startC2SAutoStartTunnels?.().catch(() => {});
}
}
function handleLogout() {
+77 -59
View File
@@ -288,48 +288,42 @@ export function AppShell({
};
window.addEventListener("termix:open-tab", handle);
return () => window.removeEventListener("termix:open-tab", handle);
}, [tabs, allHosts]);
}, [allHosts]);
// ─── Tab management ──────────────────────────────────────────────────────
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 ref = type === "terminal" ? createRef() : undefined;
if (ref) terminalRefs.current.set(tabId, ref);
const tab = {
id: tabId,
type,
label: `${host.name} (${same.length + 1})`,
host,
terminalRef: ref,
};
setTabs((prev) => {
const same = prev.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,
};
setActiveTabId(tab.id);
return [...prev, tab];
}
const next = prev.map((t) =>
t.id === same[0].id && !/\(\d+\)$/.test(t.label)
? { ...t, label: `${host.name} (1)`, host }
: t,
);
const tabId = `${host.name}-${type}-${Date.now()}`;
const ref = type === "terminal" ? createRef() : undefined;
if (ref) terminalRefs.current.set(tabId, ref);
const tab = {
id: tabId,
type,
label: `${host.name} (${same.length + 1})`,
host,
terminalRef: ref,
};
setActiveTabId(tab.id);
return [...next, tab];
});
setActiveTabId(tab.id);
}
function connectHost(host: Host, preferredType?: TabType) {
@@ -389,16 +383,26 @@ export function AppShell({
function doCloseTab(id: string) {
terminalRefs.current.delete(id);
if (id === activeTabId) {
const remaining = tabs.filter((t) => t.id !== id);
setActiveTabId(
remaining.length > 0 ? remaining[remaining.length - 1].id : "dashboard",
);
}
setTabs((prev) => {
const next = prev.filter((t) => t.id !== id);
if (id === activeTabId) setActiveTabId(next[next.length - 1].id);
if (next.length === 0)
return [
{ id: "dashboard", type: "dashboard", label: t("nav.dashboard") },
];
return next;
});
}
function closeTab(id: string) {
const tab = tabs.find((t) => t.id === id);
if (tab && SESSION_TAB_TYPES.includes(tab.type)) {
const confirmEnabled = localStorage.getItem("confirmTabClose") === "true";
if (tab && SESSION_TAB_TYPES.includes(tab.type) && confirmEnabled) {
toast(t("nav.confirmClose"), {
duration: 5000,
action: {
@@ -670,31 +674,37 @@ export function AppShell({
) : (
<>
{/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */}
{tabs
.filter((tab) => tab.type === "terminal")
.map((tab) => {
const visible = tab.id === activeTabId;
return (
<div
key={tab.id}
className="absolute inset-0 overflow-hidden"
style={{
visibility: visible ? "visible" : "hidden",
pointerEvents: visible ? "auto" : "none",
zIndex: visible ? 1 : 0,
}}
>
{renderTabContent(
tab,
openSingletonTab,
openTab,
closeTab,
visible,
)}
</div>
);
})}
{/* Non-terminal tabs: display:none when inactive */}
{(() => {
const activeTab = tabs.find((t) => t.id === activeTabId);
const nonTerminalActive =
activeTab && activeTab.type !== "terminal";
return tabs
.filter((tab) => tab.type === "terminal")
.map((tab) => {
const visible = tab.id === activeTabId;
return (
<div
key={tab.id}
className="absolute inset-0 overflow-hidden"
style={{
display: nonTerminalActive ? "none" : undefined,
visibility: visible ? "visible" : "hidden",
pointerEvents: visible ? "auto" : "none",
zIndex: visible ? 1 : 0,
}}
>
{renderTabContent(
tab,
openSingletonTab,
openTab,
closeTab,
visible,
)}
</div>
);
});
})()}
{/* Non-terminal tabs: absolutely positioned above terminals when active */}
{tabs
.filter((tab) => tab.type !== "terminal")
.map((tab) => {
@@ -702,8 +712,16 @@ export function AppShell({
return (
<div
key={tab.id}
className="flex flex-col flex-1 min-h-0 overflow-hidden"
style={{ display: visible ? undefined : "none" }}
className="flex flex-col overflow-hidden"
style={
visible
? {
position: "absolute",
inset: 0,
zIndex: 2,
}
: { display: "none" }
}
>
{renderTabContent(
tab,
+4 -4
View File
@@ -125,15 +125,15 @@ export function ElectronLoginForm({
const isEmbeddedServer = serverUrl.includes("localhost:30001");
return (
<div className="fixed inset-0 w-screen h-screen bg-canvas flex flex-col">
<div className="fixed inset-0 w-screen h-screen bg-background flex flex-col">
{isAuthenticating && (
<div className="absolute inset-0 flex items-center justify-center bg-canvas z-50">
<div className="absolute inset-0 flex items-center justify-center bg-background z-50">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)}
{!isAuthenticating && (
<div className="flex items-center justify-between p-4 bg-canvas border-b border-edge">
<div className="flex items-center justify-between p-4 bg-background border-b border-border">
<button
onClick={onChangeServer}
className="flex items-center gap-2 text-foreground hover:text-primary transition-colors"
@@ -173,7 +173,7 @@ export function ElectronLoginForm({
{loading && !isAuthenticating && (
<div
className="absolute inset-0 flex items-center justify-center bg-canvas z-40"
className="absolute inset-0 flex items-center justify-center bg-background z-40"
style={{ marginTop: "60px" }}
>
<div className="flex items-center">
+84 -115
View File
@@ -12,7 +12,6 @@ import {
type ServerConfig,
} from "@/main-axios.ts";
import { Server, Monitor, Loader2 } from "lucide-react";
import { useTheme } from "@/components/theme-provider";
interface ServerConfigProps {
onServerConfigured: (serverUrl: string) => void;
@@ -28,7 +27,6 @@ export function ElectronServerConfig({
isFirstTime = false,
}: ServerConfigProps) {
const { t } = useTranslation();
const { theme } = useTheme();
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
const [embeddedLoading, setEmbeddedLoading] = useState(false);
@@ -37,16 +35,6 @@ export function ElectronServerConfig({
null,
);
const isDarkMode =
theme === "dark" ||
theme === "dracula" ||
theme === "gentlemansChoice" ||
theme === "midnightEspresso" ||
theme === "catppuccinMocha" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
const lineColor = isDarkMode ? "#151517" : "#f9f9f9";
useEffect(() => {
loadServerConfig();
checkEmbeddedBackend();
@@ -174,62 +162,45 @@ export function ElectronServerConfig({
};
return (
<div
className="fixed inset-0 flex items-center justify-center"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="space-y-6">
<div className="text-center">
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<Server className="w-6 h-6 text-primary" />
</div>
<h2 className="text-xl font-semibold">{t("serverConfig.title")}</h2>
<p className="text-sm text-muted-foreground mt-2">
{t("serverConfig.description")}
</p>
<div className="fixed inset-0 flex items-center justify-center bg-background p-6">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2.5">
<Server className="size-4 text-accent-brand shrink-0" />
<p className="font-bold">{t("serverConfig.title")}</p>
</div>
<p className="text-sm text-muted-foreground">
{t("serverConfig.description")}
</p>
</div>
{embeddedAvailable !== false && (
<div className="space-y-2">
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleUseEmbedded}
disabled={embeddedLoading || loading}
>
{embeddedLoading ? (
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>{t("serverConfig.embeddedConnecting")}</span>
</div>
) : (
<div className="flex items-center justify-center space-x-2">
<Monitor className="w-4 h-4" />
<span>{t("serverConfig.useEmbedded")}</span>
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 rounded border border-yellow-500/30">
BETA
</span>
</div>
)}
</Button>
<p className="text-xs text-muted-foreground text-center">
{t("serverConfig.embeddedDesc")}
</p>
</div>
)}
{embeddedAvailable !== false && (
{embeddedAvailable !== false && (
<>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleUseEmbedded}
disabled={embeddedLoading || loading}
>
{embeddedLoading ? (
<span className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
{t("serverConfig.embeddedConnecting")}
</span>
) : (
<span className="flex items-center gap-2">
<Monitor className="size-4" />
{t("serverConfig.useEmbedded")}
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border border-yellow-500/30">
BETA
</span>
</span>
)}
</Button>
<p className="text-xs text-muted-foreground -mt-3">
{t("serverConfig.embeddedDesc")}
</p>
<div className="flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs text-muted-foreground">
@@ -237,63 +208,61 @@ export function ElectronServerConfig({
</span>
<div className="h-px flex-1 bg-border" />
</div>
</>
)}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1.5">
<Label htmlFor="server-url">{t("serverConfig.serverUrl")}</Label>
<Input
id="server-url"
type="text"
placeholder="https://your-server.com"
value={serverUrl}
onChange={(e) => handleUrlChange(e.target.value)}
disabled={loading || embeddedLoading}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertTitle>{t("common.error")}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="server-url">{t("serverConfig.serverUrl")}</Label>
<Input
id="server-url"
type="text"
placeholder="https://your-server.com"
value={serverUrl}
onChange={(e) => handleUrlChange(e.target.value)}
className="w-full h-10"
disabled={loading || embeddedLoading}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertTitle>{t("common.error")}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex space-x-2">
{onCancel && !isFirstTime && (
<Button
type="button"
variant="outline"
className="flex-1"
onClick={onCancel}
disabled={loading || embeddedLoading}
>
Cancel
</Button>
)}
<div className="flex gap-2">
{onCancel && !isFirstTime && (
<Button
type="button"
variant="outline"
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
onClick={handleSaveConfig}
disabled={loading || embeddedLoading || !serverUrl.trim()}
className="flex-1"
onClick={onCancel}
disabled={loading || embeddedLoading}
>
{loading ? (
<div className="flex items-center space-x-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>{t("serverConfig.saving")}</span>
</div>
) : (
t("serverConfig.saveConfig")
)}
{t("common.cancel")}
</Button>
</div>
<div className="text-xs text-muted-foreground text-center">
{t("serverConfig.helpText")}
</div>
)}
<Button
type="button"
className={`bg-accent-brand hover:bg-accent-brand/90 text-background font-bold ${onCancel && !isFirstTime ? "flex-1" : "w-full"}`}
onClick={handleSaveConfig}
disabled={loading || embeddedLoading || !serverUrl.trim()}
>
{loading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-background border-t-transparent rounded-full animate-spin" />
{t("serverConfig.saving")}
</span>
) : (
t("serverConfig.saveConfig")
)}
</Button>
</div>
<p className="text-xs text-muted-foreground text-center">
{t("serverConfig.helpText")}
</p>
</div>
</div>
</div>
+43 -18
View File
@@ -35,6 +35,7 @@ import {
getServerMetricsById,
registerMetricsViewer,
sendMetricsHeartbeat,
getUserInfo,
} from "@/main-axios";
import type { RecentActivityItem, SSHHostWithStatus } from "@/main-axios";
import { useTranslation } from "react-i18next";
@@ -278,10 +279,12 @@ function QuickActionsCard({
onOpenSingletonTab,
hosts,
onOpenTab,
isAdmin,
}: {
onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void;
hosts: Host[];
onOpenTab: (host: Host, type: TabType) => void;
isAdmin: boolean;
}) {
const { t } = useTranslation();
const pinnedHosts = hosts.filter((h) => h.pin);
@@ -357,22 +360,24 @@ function QuickActionsCard({
</div>
) : (
<>
<button
onClick={() => onOpenSingletonTab("admin-settings")}
className="group/btn flex items-center gap-2.5 px-4 py-2.5 hover:bg-muted transition-colors cursor-pointer border-b border-border flex-1"
>
<div className="size-7 border border-border bg-muted flex items-center justify-center shrink-0 group-hover/btn:bg-accent-brand/20 group-hover/btn:border-accent-brand/40 transition-colors">
<Settings className="size-3 text-accent-brand" />
</div>
<div className="flex flex-col items-start text-left">
<span className="text-xs font-semibold">
{t("dashboard.adminSettings")}
</span>
<span className="text-[10px] text-muted-foreground">
{t("dashboardTab.manageUsersAndRoles")}
</span>
</div>
</button>
{isAdmin && (
<button
onClick={() => onOpenSingletonTab("admin-settings")}
className="group/btn flex items-center gap-2.5 px-4 py-2.5 hover:bg-muted transition-colors cursor-pointer border-b border-border flex-1"
>
<div className="size-7 border border-border bg-muted flex items-center justify-center shrink-0 group-hover/btn:bg-accent-brand/20 group-hover/btn:border-accent-brand/40 transition-colors">
<Settings className="size-3 text-accent-brand" />
</div>
<div className="flex flex-col items-start text-left">
<span className="text-xs font-semibold">
{t("dashboard.adminSettings")}
</span>
<span className="text-[10px] text-muted-foreground">
{t("dashboardTab.manageUsersAndRoles")}
</span>
</div>
</button>
)}
<button
onClick={() => onOpenSingletonTab("user-profile")}
className="group/btn flex items-center gap-2.5 px-4 py-2.5 hover:bg-muted transition-colors cursor-pointer flex-1"
@@ -648,6 +653,7 @@ function CardItem({
activeTunnelCount,
activity,
onClearActivity,
isAdmin,
}: {
slot: CardSlot;
editMode: boolean;
@@ -669,6 +675,7 @@ function CardItem({
activeTunnelCount: number;
activity: RecentActivityItem[];
onClearActivity: () => void;
isAdmin: boolean;
}) {
const cardRef = useRef<HTMLDivElement | null>(null);
@@ -737,13 +744,18 @@ function CardItem({
/>
)}
{slot.id === "quick_actions" && (
<QuickActionsCard onOpenSingletonTab={onOpenSingletonTab} hosts={hosts} onOpenTab={onOpenTab} />
<QuickActionsCard
onOpenSingletonTab={onOpenSingletonTab}
hosts={hosts}
onOpenTab={onOpenTab}
/>
)}
{slot.id === "host_status" && (
<HostStatusCard
hosts={hosts}
hostMetrics={hostMetrics}
onOpenTab={onOpenTab}
isAdmin={isAdmin}
/>
)}
{slot.id === "recent_activity" && (
@@ -866,6 +878,7 @@ type PanelColumnProps = {
activity: RecentActivityItem[];
onClearActivity: () => void;
cardLabels: Record<DashboardCardId, string>;
isAdmin: boolean;
};
function PanelColumn({
@@ -892,6 +905,7 @@ function PanelColumn({
activity,
onClearActivity,
cardLabels,
isAdmin,
}: PanelColumnProps) {
const { t } = useTranslation();
const sorted = [...slots].sort((a, b) => a.order - b.order);
@@ -943,6 +957,7 @@ function PanelColumn({
activeTunnelCount={activeTunnelCount}
activity={activity}
onClearActivity={onClearActivity}
isAdmin={isAdmin}
/>
</div>
))}
@@ -1039,6 +1054,7 @@ export function DashboardTab({
}, [mainWidthPct]);
const [hosts, setHosts] = useState<Host[]>([]);
const [isAdmin, setIsAdmin] = useState(false);
const [uptimeFormatted, setUptimeFormatted] = useState("");
const [versionText, setVersionText] = useState("");
const [versionStatus, setVersionStatus] = useState<
@@ -1112,6 +1128,9 @@ export function DashboardTab({
};
load();
getUserInfo()
.then((info) => setIsAdmin(!!info.is_admin))
.catch(() => {});
getUptime()
.then((u) => setUptimeFormatted(u.formatted))
.catch(() => {});
@@ -1312,6 +1331,7 @@ export function DashboardTab({
onOpenSingletonTab,
onOpenTab,
cardLabels,
isAdmin,
};
const isMobile = useIsMobile();
@@ -1395,13 +1415,18 @@ export function DashboardTab({
/>
)}
{slot.id === "quick_actions" && (
<QuickActionsCard onOpenSingletonTab={onOpenSingletonTab} hosts={hosts} onOpenTab={onOpenTab} />
<QuickActionsCard
onOpenSingletonTab={onOpenSingletonTab}
hosts={hosts}
onOpenTab={onOpenTab}
/>
)}
{slot.id === "host_status" && (
<HostStatusCard
hosts={hosts}
hostMetrics={hostMetrics}
onOpenTab={onOpenTab}
isAdmin={isAdmin}
/>
)}
{slot.id === "recent_activity" && (
@@ -0,0 +1,121 @@
import { useTheme } from "@/components/theme-provider";
import { TERMINAL_THEMES, TERMINAL_FONTS } from "@/lib/terminal-themes";
interface TerminalPreviewProps {
theme: string;
fontSize?: number;
fontFamily?: string;
cursorStyle?: "block" | "underline" | "bar";
cursorBlink?: boolean;
letterSpacing?: number;
lineHeight?: number;
}
export function TerminalPreview({
theme = "termix",
fontSize = 14,
fontFamily = "Caskaydia Cove Nerd Font Mono",
cursorStyle = "bar",
cursorBlink = true,
letterSpacing = 0,
lineHeight = 1.0,
}: TerminalPreviewProps) {
const { theme: appTheme } = useTheme();
const resolvedTheme =
theme === "termix"
? appTheme === "dark" ||
(appTheme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
? "termixDark"
: "termixLight"
: theme;
const colors = TERMINAL_THEMES[resolvedTheme]?.colors;
const fontFallback =
TERMINAL_FONTS.find((f) => f.value === fontFamily)?.fallback ||
TERMINAL_FONTS[0].fallback;
return (
<div className="border border-input overflow-hidden">
<div
className="p-3 font-mono"
style={{
fontSize: `${fontSize}px`,
fontFamily: fontFallback,
letterSpacing: `${letterSpacing}px`,
lineHeight,
background: colors?.background || "var(--bg-base)",
color: colors?.foreground || "var(--foreground)",
}}
>
<div>
<span style={{ color: colors?.green }}>deploy@web-01</span>
<span style={{ color: colors?.brightBlack }}>:</span>
<span style={{ color: colors?.blue }}>~</span>
<span style={{ color: colors?.brightBlack }}>$</span>
<span> ls -la</span>
</div>
<div style={{ color: colors?.brightBlack }}>total 48</div>
<div>
<span style={{ color: colors?.cyan }}>drwxr-xr-x</span>
<span style={{ color: colors?.brightBlack }}>
{" "}
5 deploy deploy 4096 May 1 09:12{" "}
</span>
<span style={{ color: colors?.blue }}>.</span>
</div>
<div>
<span style={{ color: colors?.cyan }}>drwxr-xr-x</span>
<span style={{ color: colors?.brightBlack }}>
{" "}
3 root root 4096 Apr 15 18:44{" "}
</span>
<span style={{ color: colors?.blue }}>..</span>
</div>
<div>
<span style={{ color: colors?.cyan }}>-rw-r--r--</span>
<span style={{ color: colors?.brightBlack }}>
{" "}
1 deploy deploy 220 Apr 15 18:44{" "}
</span>
<span>.bash_logout</span>
</div>
<div>
<span style={{ color: colors?.cyan }}>-rwxr-xr-x</span>
<span style={{ color: colors?.brightBlack }}>
{" "}
1 deploy deploy 8192 May 1 08:55{" "}
</span>
<span style={{ color: colors?.green }}>deploy.sh</span>
</div>
<div className="flex items-center gap-0.5 mt-0.5">
<span style={{ color: colors?.green }}>deploy@web-01</span>
<span style={{ color: colors?.brightBlack }}>:</span>
<span style={{ color: colors?.blue }}>~</span>
<span style={{ color: colors?.brightBlack }}>$</span>
<span> </span>
<span
className="inline-block"
style={{
width: cursorStyle === "block" ? "0.6em" : "0.12em",
height: cursorStyle === "underline" ? "0.12em" : `${fontSize}px`,
background: colors?.cursor || colors?.foreground || "#f7f7f7",
animation: cursorBlink
? "termPreviewBlink 1s step-end infinite"
: "none",
verticalAlign:
cursorStyle === "underline" ? "bottom" : "text-bottom",
}}
/>
</div>
</div>
<style>{`
@keyframes termPreviewBlink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
`}</style>
</div>
);
}
+9 -5
View File
@@ -137,7 +137,9 @@
"history": "History",
"hosts": "Hosts",
"snippets": "Snippets",
"hostManager": "Host Manager"
"hostManager": "Host Manager",
"roleAdministrator": "Administrator",
"roleUser": "User"
},
"hosts": {
"hosts": "Hosts",
@@ -459,7 +461,7 @@
"addTagsPlaceholder": "Add tags...",
"authDetails": "Authentication Details",
"credType": "Type",
"generateKeyPairDesc": "Generate a new key pair both private and public keys will be filled automatically.",
"generateKeyPairDesc": "Generate a new key pair, both private and public keys will be filled automatically.",
"generatingKey": "Generating...",
"generateLabel": "Generate {{label}}",
"uploadFileBtn": "Upload file",
@@ -490,7 +492,7 @@
"connectionLabel": "Connection",
"authenticationLabel": "Authentication",
"generateKeyPairTitle": "Generate Key Pair",
"generateKeyPairDescription": "Generate a new key pair both private and public keys will be filled automatically.",
"generateKeyPairDescription": "Generate a new key pair, both private and public keys will be filled automatically.",
"generateFromPrivateKey": "Generate from Private Key",
"refreshBtn2": "Refresh",
"exitSelectionTitle": "Exit selection",
@@ -690,7 +692,7 @@
"toggleWith": "Toggle with"
},
"splitScreen": {
"paneEmpty": "Pane {{index}} empty",
"paneEmpty": "Pane {{index}} - empty",
"noTabAssigned": "No tab assigned"
},
"guacamole": {
@@ -1396,7 +1398,7 @@
"tunnel": "Tunnel",
"docker": "Docker",
"serverStats": "Server Stats",
"noNodes": "No nodes yet — add hosts to build your topology"
"noNodes": "No nodes yet"
},
"docker": {
"notEnabled": "Docker is not enabled for this host",
@@ -1646,6 +1648,8 @@
"commandPaletteDesc": "Enable keyboard shortcut",
"sessionPersistence": "Session Persistence",
"sessionPersistenceDesc": "Keep sessions between reconnects",
"confirmTabClose": "Confirm Tab Close",
"confirmTabCloseDesc": "Ask before closing terminal tabs",
"settingsSidebar": "Sidebar",
"showHostTags": "Show Host Tags",
"showHostTagsDesc": "Display tags in host list",
+1 -1
View File
@@ -209,7 +209,7 @@ export function AppRail({
{username || "User"}
</span>
<span className="text-[10px] text-muted-foreground leading-tight whitespace-nowrap">
Administrator
{isAdmin ? t("nav.roleAdministrator") : t("nav.roleUser")}
</span>
</div>
</button>
+141 -56
View File
@@ -64,6 +64,7 @@ import {
} from "@/components/dropdown-menu";
import { toast } from "sonner";
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
import { TerminalPreview } from "@/features/terminal/TerminalPreview";
import {
getSSHHosts,
getCredentials,
@@ -2047,58 +2048,15 @@ function HostEditor({
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t("hosts.themePreview")}
</label>
<div className="w-full bg-[#111210] border border-border font-mono text-xs leading-relaxed overflow-hidden">
<div className="px-3 py-2.5 flex flex-col gap-0.5">
<div>
<span className="text-[#5af78e]">deploy@web-01</span>
<span className="text-[#555]">:</span>
<span className="text-[#57c7ff]">~</span>
<span className="text-[#555]">$</span>
<span className="text-[#f1f1f0]"> ls -la</span>
</div>
<div className="text-[#555]">total 48</div>
<div>
<span className="text-[#9aedfe]">drwxr-xr-x</span>
<span className="text-[#555]">
{" "}
5 deploy deploy 4096 May 1 09:12{" "}
</span>
<span className="text-[#57c7ff]">.</span>
</div>
<div>
<span className="text-[#9aedfe]">drwxr-xr-x</span>
<span className="text-[#555]">
{" "}
3 root root 4096 Apr 15 18:44{" "}
</span>
<span className="text-[#57c7ff]">..</span>
</div>
<div>
<span className="text-[#9aedfe]">-rw-r--r--</span>
<span className="text-[#555]">
{" "}
1 deploy deploy 220 Apr 15 18:44{" "}
</span>
<span className="text-[#f1f1f0]">.bash_logout</span>
</div>
<div>
<span className="text-[#9aedfe]">-rwxr-xr-x</span>
<span className="text-[#555]">
{" "}
1 deploy deploy 8192 May 1 08:55{" "}
</span>
<span className="text-[#5af78e]">deploy.sh</span>
</div>
<div className="flex items-center gap-0.5 mt-0.5">
<span className="text-[#5af78e]">deploy@web-01</span>
<span className="text-[#555]">:</span>
<span className="text-[#57c7ff]">~</span>
<span className="text-[#555]">$</span>
<span className="text-[#f1f1f0]"> </span>
<span className="inline-block w-1.5 h-3.5 bg-[#f1f1f0] animate-pulse" />
</div>
</div>
</div>
<TerminalPreview
theme={form.theme}
fontSize={form.fontSize}
fontFamily={form.fontFamily}
cursorStyle={form.cursorStyle}
cursorBlink={form.cursorBlink}
letterSpacing={form.letterSpacing}
lineHeight={form.lineHeight}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
@@ -5506,19 +5464,146 @@ export function HostManager({
{
hosts: [
{
name: "My Server",
ip: "192.168.1.1",
username: "root",
port: 22,
name: "Web Server (Production)",
ip: "192.168.1.100",
username: "admin",
authType: "password",
password: "your_secure_password_here",
folder: "Production",
tags: ["web", "production", "nginx"],
pin: true,
notes: "Main production web server running Nginx",
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 22,
enableTerminal: true,
enableTunnel: false,
enableFileManager: true,
enableDocker: false,
defaultPath: "/var/www",
},
{
name: "Database Server",
ip: "192.168.1.101",
username: "dbadmin",
authType: "key",
key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----",
keyPassword: "optional_key_passphrase",
keyType: "ssh-ed25519",
folder: "Production",
tags: ["database", "production", "postgresql"],
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 22,
enableTerminal: true,
enableTunnel: true,
enableFileManager: false,
enableDocker: false,
tunnelConnections: [
{
sourcePort: 5432,
endpointPort: 5432,
endpointHost: "localhost",
maxRetries: 3,
retryInterval: 10,
autoStart: true,
},
],
statsConfig: {
enabledWidgets: ["cpu", "memory", "disk", "network", "uptime"],
statusCheckEnabled: true,
statusCheckInterval: 30,
metricsEnabled: true,
metricsInterval: 30,
},
},
{
name: "Development Server",
ip: "192.168.1.102",
username: "developer",
authType: "password",
password: "dev_password",
folder: "Development",
tags: ["dev", "testing"],
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 2222,
enableTerminal: true,
enableTunnel: false,
enableFileManager: true,
enableDocker: true,
defaultPath: "/home/developer",
},
{
name: "Windows Server 2022",
ip: "192.168.1.200",
username: "Administrator",
folder: "Remote Desktop",
tags: ["rdp", "windows", "production"],
enableSsh: false,
enableRdp: true,
enableVnc: false,
enableTelnet: false,
rdpPort: 3389,
rdpUser: "Administrator",
rdpPassword: "windows_password",
rdpDomain: "COMPANY",
rdpSecurity: "nla",
rdpIgnoreCert: false,
},
{
name: "Ubuntu Desktop",
ip: "192.168.1.201",
username: "vncuser",
folder: "Remote Desktop",
tags: ["vnc", "linux", "desktop"],
enableSsh: false,
enableRdp: false,
enableVnc: true,
enableTelnet: false,
vncPort: 5900,
vncPassword: "vnc_password",
},
{
name: "Network Switch",
ip: "192.168.1.254",
username: "admin",
folder: "Infrastructure",
tags: ["telnet", "network", "switch"],
enableSsh: false,
enableRdp: false,
enableVnc: false,
enableTelnet: true,
telnetPort: 23,
telnetUser: "admin",
telnetPassword: "switch_password",
},
{
name: "Server with SOCKS5 Proxy",
ip: "10.10.10.100",
username: "proxyuser",
authType: "password",
password: "secure_password",
folder: "Proxied Hosts",
tags: ["proxy", "socks5"],
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 22,
enableTerminal: true,
enableFileManager: true,
useSocks5: true,
socks5Host: "proxy.example.com",
socks5Port: 1080,
socks5Username: "proxyauth",
socks5Password: "proxypass",
},
],
},
+7 -1
View File
@@ -139,7 +139,13 @@ export function HostItem({
className={`relative flex items-stretch cursor-pointer select-none transition-colors hover:bg-muted/40 ${stripeIndex % 2 === 1 ? "bg-muted/20" : ""}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => onOpenTab("terminal")}
onClick={() => {
if (host.enableSsh) onOpenTab("terminal");
else if (host.enableRdp) onOpenTab("rdp");
else if (host.enableVnc) onOpenTab("vnc");
else if (host.enableTelnet) onOpenTab("telnet");
else onOpenTab("terminal");
}}
>
{/* Status stripe */}
<div
+15
View File
@@ -480,6 +480,9 @@ export function UserProfilePanel({
const [disableUpdateCheck, setDisableUpdateCheck] = useState(
() => localStorage.getItem("disableUpdateCheck") === "true",
);
const [confirmTabClose, setConfirmTabClose] = useState(
() => localStorage.getItem("confirmTabClose") === "true",
);
// API keys
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
@@ -1008,6 +1011,18 @@ export function UserProfilePanel({
}}
/>
</SettingRow>
<SettingRow
label={t("newUi.sidebar.userProfile.confirmTabClose")}
description={t("newUi.sidebar.userProfile.confirmTabCloseDesc")}
>
<FakeSwitch
checked={confirmTabClose}
onChange={(v) => {
setConfirmTabClose(v);
localStorage.setItem("confirmTabClose", v.toString());
}}
/>
</SettingRow>
</div>
<div className="flex flex-col gap-1 border-t border-border pt-3">
+30 -91
View File
@@ -4,7 +4,6 @@ import { VersionAlert } from "@/components/version-alert.tsx";
import { useTranslation } from "react-i18next";
import { isElectron } from "@/lib/electron";
import { checkElectronUpdate } from "@/main-axios.ts";
import { useTheme } from "@/components/theme-provider";
interface VersionCheckModalProps {
onContinue: () => void;
@@ -18,7 +17,6 @@ type ElectronWindow = Window & {
export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
const { t } = useTranslation();
const { theme } = useTheme();
const [versionInfo, setVersionInfo] = useState<Record<
string,
unknown
@@ -26,15 +24,6 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed] = useState(false);
const isDarkMode =
theme === "dark" ||
theme === "dracula" ||
theme === "gentlemansChoice" ||
theme === "midnightEspresso" ||
theme === "catppuccinMocha" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
const lineColor = isDarkMode ? "#151517" : "#f9f9f9";
const versionModalTitle =
versionInfo?.status === "beta"
? t("versionCheck.betaVersion")
@@ -112,24 +101,10 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
if (versionChecking && !versionInfo) {
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="flex items-center justify-center mb-4">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
<p className="text-center text-muted-foreground">
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full items-center">
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-muted-foreground">
{t("versionCheck.checkingUpdates")}
</p>
</div>
@@ -139,76 +114,40 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) {
if (!versionInfo || versionDismissed) {
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.checkUpdates")}
</h2>
</div>
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
<p className="font-bold">{t("versionCheck.checkUpdates")}</p>
{versionInfo && !versionDismissed && (
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
</div>
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
)}
<div className="flex gap-2">
<Button onClick={handleContinue} className="flex-1 h-10">
{t("common.continue")}
</Button>
</div>
<Button
onClick={handleContinue}
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
>
{t("common.continue")}
</Button>
</div>
</div>
);
}
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{
background: "var(--bg-elevated)",
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
${lineColor} 35px,
${lineColor} 37px
)`,
}}
>
<div className="w-[420px] max-w-full p-8 flex flex-col backdrop-blur-sm bg-card/50 rounded-2xl shadow-xl border-2 border-edge overflow-y-auto thin-scrollbar my-2 animate-in fade-in zoom-in-95 duration-300">
<div className="mb-4">
<h2 className="text-lg font-semibold">{versionModalTitle}</h2>
</div>
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleContinue} className="flex-1 h-10">
{t("common.continue")}
</Button>
</div>
<div className="fixed inset-0 flex items-center justify-center bg-background p-6 z-50">
<div className="flex flex-col gap-5 p-6 border border-border bg-card max-w-md w-full">
<p className="font-bold">{versionModalTitle}</p>
<VersionAlert
updateInfo={versionInfo}
onDownload={handleDownloadUpdate}
/>
<Button
onClick={handleContinue}
className="w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"
>
{t("common.continue")}
</Button>
</div>
</div>
);