2025-11-05 10:36:16 -06:00
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
2025-12-31 22:20:12 -06:00
|
|
|
import { HostManagerViewer } from "@/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx";
|
2025-09-12 14:42:00 -05:00
|
|
|
import {
|
|
|
|
|
Tabs,
|
|
|
|
|
TabsContent,
|
|
|
|
|
TabsList,
|
|
|
|
|
TabsTrigger,
|
|
|
|
|
} from "@/components/ui/tabs.tsx";
|
|
|
|
|
import { Separator } from "@/components/ui/separator.tsx";
|
2025-12-31 22:20:12 -06:00
|
|
|
import { HostManagerEditor } from "@/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx";
|
|
|
|
|
import { CredentialsManager } from "@/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx";
|
|
|
|
|
import { CredentialEditor } from "@/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx";
|
2025-09-12 14:42:00 -05:00
|
|
|
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2026-04-22 16:55:23 -05:00
|
|
|
import { exportSSHHostWithCredentials } from "@/ui/main-axios.ts";
|
2025-09-12 14:42:00 -05:00
|
|
|
import type { SSHHost, HostManagerProps } from "../../../types/index";
|
|
|
|
|
|
|
|
|
|
export function HostManager({
|
|
|
|
|
isTopbarOpen,
|
2026-02-12 22:28:13 -06:00
|
|
|
initialTab = "hosts",
|
2025-11-05 10:36:16 -06:00
|
|
|
hostConfig,
|
2025-12-31 22:20:12 -06:00
|
|
|
_updateTimestamp,
|
2025-11-17 09:46:05 -06:00
|
|
|
rightSidebarOpen = false,
|
|
|
|
|
rightSidebarWidth = 400,
|
2025-12-31 22:20:12 -06:00
|
|
|
currentTabId,
|
|
|
|
|
updateTab,
|
2025-09-12 14:42:00 -05:00
|
|
|
}: HostManagerProps): React.ReactElement {
|
|
|
|
|
const { t } = useTranslation();
|
2026-02-12 22:28:13 -06:00
|
|
|
const [activeTab, setActiveTab] = useState(
|
|
|
|
|
initialTab === "host_viewer" || initialTab === "add_host"
|
|
|
|
|
? "hosts"
|
|
|
|
|
: initialTab === "credentials" || initialTab === "add_credential"
|
|
|
|
|
? "credentials"
|
|
|
|
|
: initialTab,
|
|
|
|
|
);
|
2025-11-05 10:36:16 -06:00
|
|
|
const [editingHost, setEditingHost] = useState<SSHHost | null>(
|
|
|
|
|
hostConfig || null,
|
|
|
|
|
);
|
2026-02-12 22:28:13 -06:00
|
|
|
const [isAddingHost, setIsAddingHost] = useState(false);
|
|
|
|
|
const [isAddingCredential, setIsAddingCredential] = useState(false);
|
2025-09-12 14:42:00 -05:00
|
|
|
|
2025-12-31 22:20:12 -06:00
|
|
|
useEffect(() => {}, [editingHost]);
|
|
|
|
|
|
2025-11-05 10:36:16 -06:00
|
|
|
const [editingCredential, setEditingCredential] = useState<{
|
|
|
|
|
id: number;
|
|
|
|
|
name?: string;
|
|
|
|
|
username: string;
|
|
|
|
|
} | null>(null);
|
2025-09-12 14:42:00 -05:00
|
|
|
const { state: sidebarState } = useSidebar();
|
2025-11-05 10:36:16 -06:00
|
|
|
const ignoreNextHostConfigChangeRef = useRef<boolean>(false);
|
|
|
|
|
const lastProcessedHostIdRef = useRef<number | undefined>(undefined);
|
|
|
|
|
|
2026-02-12 22:28:13 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleAddHostEvent = () => {
|
|
|
|
|
setActiveTab("hosts");
|
|
|
|
|
setEditingHost(null);
|
|
|
|
|
setIsAddingHost(true);
|
|
|
|
|
setIsAddingCredential(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddCredentialEvent = () => {
|
|
|
|
|
setActiveTab("credentials");
|
|
|
|
|
setEditingCredential(null);
|
|
|
|
|
setIsAddingCredential(true);
|
|
|
|
|
setIsAddingHost(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener("host-manager:add-host", handleAddHostEvent);
|
|
|
|
|
window.addEventListener(
|
|
|
|
|
"host-manager:add-credential",
|
|
|
|
|
handleAddCredentialEvent,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("host-manager:add-host", handleAddHostEvent);
|
|
|
|
|
window.removeEventListener(
|
|
|
|
|
"host-manager:add-credential",
|
|
|
|
|
handleAddCredentialEvent,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-11-05 10:36:16 -06:00
|
|
|
useEffect(() => {
|
2025-12-31 22:20:12 -06:00
|
|
|
if (_updateTimestamp !== undefined) {
|
2026-02-12 22:28:13 -06:00
|
|
|
const normalizedTab =
|
|
|
|
|
initialTab === "host_viewer" || initialTab === "add_host"
|
|
|
|
|
? "hosts"
|
|
|
|
|
: initialTab === "credentials" || initialTab === "add_credential"
|
|
|
|
|
? "credentials"
|
|
|
|
|
: initialTab;
|
|
|
|
|
|
|
|
|
|
if (initialTab && normalizedTab !== activeTab) {
|
|
|
|
|
setActiveTab(normalizedTab);
|
2025-12-31 22:20:12 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 22:28:13 -06:00
|
|
|
if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) {
|
2025-12-31 22:20:12 -06:00
|
|
|
lastProcessedHostIdRef.current = hostConfig.id;
|
2026-04-22 16:55:23 -05:00
|
|
|
setIsAddingHost(false);
|
|
|
|
|
exportSSHHostWithCredentials(hostConfig.id)
|
|
|
|
|
.then((fullHost) =>
|
|
|
|
|
setEditingHost({ ...hostConfig, ...fullHost } as SSHHost),
|
|
|
|
|
)
|
|
|
|
|
.catch(() => setEditingHost(hostConfig));
|
2026-02-12 22:28:13 -06:00
|
|
|
} else if (!hostConfig && editingHost) {
|
2025-12-31 22:20:12 -06:00
|
|
|
setEditingHost(null);
|
2026-02-12 22:28:13 -06:00
|
|
|
setIsAddingHost(false);
|
2025-12-31 22:20:12 -06:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (initialTab) {
|
2026-02-12 22:28:13 -06:00
|
|
|
const normalizedTab =
|
|
|
|
|
initialTab === "host_viewer" || initialTab === "add_host"
|
|
|
|
|
? "hosts"
|
|
|
|
|
: initialTab === "credentials" || initialTab === "add_credential"
|
|
|
|
|
? "credentials"
|
|
|
|
|
: initialTab;
|
|
|
|
|
setActiveTab(normalizedTab);
|
2025-12-31 22:20:12 -06:00
|
|
|
}
|
2026-02-12 22:28:13 -06:00
|
|
|
if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) {
|
2025-12-31 22:20:12 -06:00
|
|
|
lastProcessedHostIdRef.current = hostConfig.id;
|
2026-04-22 16:55:23 -05:00
|
|
|
setIsAddingHost(false);
|
|
|
|
|
exportSSHHostWithCredentials(hostConfig.id)
|
|
|
|
|
.then((fullHost) =>
|
|
|
|
|
setEditingHost({ ...hostConfig, ...fullHost } as SSHHost),
|
|
|
|
|
)
|
|
|
|
|
.catch(() => setEditingHost(hostConfig));
|
2025-12-31 22:20:12 -06:00
|
|
|
}
|
2025-11-05 10:36:16 -06:00
|
|
|
}
|
2026-02-12 22:28:13 -06:00
|
|
|
}, [_updateTimestamp, initialTab, hostConfig]);
|
2025-09-12 14:42:00 -05:00
|
|
|
|
2026-04-22 16:55:23 -05:00
|
|
|
const handleEditHost = async (host: SSHHost) => {
|
2026-02-12 22:28:13 -06:00
|
|
|
setIsAddingHost(false);
|
2025-11-05 10:36:16 -06:00
|
|
|
lastProcessedHostIdRef.current = host.id;
|
2026-04-22 16:55:23 -05:00
|
|
|
try {
|
|
|
|
|
const fullHost = await exportSSHHostWithCredentials(host.id);
|
|
|
|
|
setEditingHost({ ...host, ...fullHost } as SSHHost);
|
|
|
|
|
} catch {
|
|
|
|
|
setEditingHost(host);
|
|
|
|
|
}
|
2026-02-12 22:28:13 -06:00
|
|
|
};
|
2025-12-31 22:20:12 -06:00
|
|
|
|
2026-02-12 22:28:13 -06:00
|
|
|
const handleAddHost = () => {
|
|
|
|
|
setEditingHost(null);
|
|
|
|
|
setIsAddingHost(true);
|
|
|
|
|
lastProcessedHostIdRef.current = undefined;
|
2025-09-12 14:42:00 -05:00
|
|
|
};
|
|
|
|
|
|
2026-05-06 15:12:07 -05:00
|
|
|
const handleFormSubmit = (savedHost?: SSHHost) => {
|
2025-11-05 10:36:16 -06:00
|
|
|
ignoreNextHostConfigChangeRef.current = true;
|
2026-05-06 15:12:07 -05:00
|
|
|
const isUpdatingHost = Boolean(editingHost?.id && savedHost?.id);
|
|
|
|
|
|
|
|
|
|
if (isUpdatingHost) {
|
|
|
|
|
setEditingHost((current) => ({ ...current, ...savedHost }) as SSHHost);
|
|
|
|
|
setIsAddingHost(false);
|
|
|
|
|
lastProcessedHostIdRef.current = savedHost.id;
|
|
|
|
|
if (updateTab && currentTabId !== undefined) {
|
|
|
|
|
updateTab(currentTabId, { hostConfig: savedHost });
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const savedHostId = savedHost?.id || editingHost?.id;
|
2025-09-12 14:42:00 -05:00
|
|
|
setEditingHost(null);
|
2026-02-12 22:28:13 -06:00
|
|
|
setIsAddingHost(false);
|
2025-11-05 10:36:16 -06:00
|
|
|
setTimeout(() => {
|
2026-02-12 22:28:13 -06:00
|
|
|
lastProcessedHostIdRef.current = savedHostId;
|
2025-11-05 10:36:16 -06:00
|
|
|
}, 500);
|
2025-09-12 14:42:00 -05:00
|
|
|
};
|
|
|
|
|
|
2025-11-05 10:36:16 -06:00
|
|
|
const handleEditCredential = (credential: {
|
|
|
|
|
id: number;
|
|
|
|
|
name?: string;
|
|
|
|
|
username: string;
|
|
|
|
|
}) => {
|
2025-09-12 14:42:00 -05:00
|
|
|
setEditingCredential(credential);
|
2026-02-12 22:28:13 -06:00
|
|
|
setIsAddingCredential(false);
|
|
|
|
|
};
|
2025-12-31 22:20:12 -06:00
|
|
|
|
2026-02-12 22:28:13 -06:00
|
|
|
const handleAddCredential = () => {
|
|
|
|
|
setEditingCredential(null);
|
|
|
|
|
setIsAddingCredential(true);
|
2025-09-12 14:42:00 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCredentialFormSubmit = () => {
|
|
|
|
|
setEditingCredential(null);
|
2026-02-12 22:28:13 -06:00
|
|
|
setIsAddingCredential(false);
|
2025-09-12 14:42:00 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTabChange = (value: string) => {
|
2026-02-12 22:28:13 -06:00
|
|
|
if (activeTab !== value) {
|
2025-09-12 14:42:00 -05:00
|
|
|
setEditingHost(null);
|
2026-02-12 22:28:13 -06:00
|
|
|
setEditingCredential(null);
|
|
|
|
|
setIsAddingHost(false);
|
|
|
|
|
setIsAddingCredential(false);
|
2026-01-24 19:49:42 -06:00
|
|
|
lastProcessedHostIdRef.current = undefined;
|
|
|
|
|
|
|
|
|
|
if (updateTab && currentTabId !== undefined) {
|
|
|
|
|
updateTab(currentTabId, { hostConfig: null });
|
|
|
|
|
}
|
2025-09-12 14:42:00 -05:00
|
|
|
}
|
2026-01-24 19:49:42 -06:00
|
|
|
|
2025-11-17 09:46:05 -06:00
|
|
|
setActiveTab(value);
|
2025-12-31 22:20:12 -06:00
|
|
|
|
|
|
|
|
if (updateTab && currentTabId !== undefined) {
|
|
|
|
|
updateTab(currentTabId, { initialTab: value });
|
|
|
|
|
}
|
2025-09-12 14:42:00 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const topMarginPx = isTopbarOpen ? 74 : 26;
|
|
|
|
|
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
|
|
|
|
const bottomMarginPx = 8;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
<div
|
2025-12-31 22:20:12 -06:00
|
|
|
className="bg-canvas text-foreground p-4 pt-0 rounded-lg border-2 border-edge flex flex-col min-h-0 overflow-hidden"
|
2025-09-12 14:42:00 -05:00
|
|
|
style={{
|
|
|
|
|
marginLeft: leftMarginPx,
|
2025-11-17 09:46:05 -06:00
|
|
|
marginRight: rightSidebarOpen
|
|
|
|
|
? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)`
|
|
|
|
|
: 17,
|
2025-09-12 14:42:00 -05:00
|
|
|
marginTop: topMarginPx,
|
|
|
|
|
marginBottom: bottomMarginPx,
|
|
|
|
|
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
2025-11-17 09:46:05 -06:00
|
|
|
transition:
|
|
|
|
|
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
2025-09-12 14:42:00 -05:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Tabs
|
|
|
|
|
value={activeTab}
|
|
|
|
|
onValueChange={handleTabChange}
|
|
|
|
|
className="flex-1 flex flex-col h-full min-h-0"
|
|
|
|
|
>
|
2025-12-31 22:20:12 -06:00
|
|
|
<TabsList className="bg-elevated border-2 border-edge mt-1.5">
|
|
|
|
|
<TabsTrigger
|
2026-02-12 22:28:13 -06:00
|
|
|
value="hosts"
|
2025-12-31 22:20:12 -06:00
|
|
|
className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
|
|
|
|
>
|
2026-02-12 22:28:13 -06:00
|
|
|
{t("hosts.hosts")}
|
2025-09-12 14:42:00 -05:00
|
|
|
</TabsTrigger>
|
2025-12-31 22:20:12 -06:00
|
|
|
<TabsTrigger
|
|
|
|
|
value="credentials"
|
|
|
|
|
className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
|
|
|
|
>
|
2026-02-12 22:28:13 -06:00
|
|
|
{t("credentials.credentials")}
|
2025-09-12 14:42:00 -05:00
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
<TabsContent
|
2026-02-12 22:28:13 -06:00
|
|
|
value="hosts"
|
2025-09-12 14:42:00 -05:00
|
|
|
className="flex-1 flex flex-col h-full min-h-0"
|
|
|
|
|
>
|
|
|
|
|
<Separator className="p-0.25 -mt-0.5 mb-1" />
|
2026-02-12 22:28:13 -06:00
|
|
|
{editingHost !== null || isAddingHost ? (
|
|
|
|
|
<div className="flex flex-col h-full min-h-0">
|
|
|
|
|
<HostManagerEditor
|
|
|
|
|
editingHost={editingHost}
|
|
|
|
|
onFormSubmit={handleFormSubmit}
|
|
|
|
|
onBack={() => {
|
|
|
|
|
setEditingHost(null);
|
|
|
|
|
setIsAddingHost(false);
|
|
|
|
|
lastProcessedHostIdRef.current = undefined;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<HostManagerViewer
|
|
|
|
|
onEditHost={handleEditHost}
|
|
|
|
|
onAddHost={handleAddHost}
|
2025-09-12 14:42:00 -05:00
|
|
|
/>
|
2026-02-12 22:28:13 -06:00
|
|
|
)}
|
2025-09-12 14:42:00 -05:00
|
|
|
</TabsContent>
|
|
|
|
|
<TabsContent
|
|
|
|
|
value="credentials"
|
|
|
|
|
className="flex-1 flex flex-col h-full min-h-0"
|
|
|
|
|
>
|
|
|
|
|
<Separator className="p-0.25 -mt-0.5 mb-1" />
|
2026-02-12 22:28:13 -06:00
|
|
|
{editingCredential !== null || isAddingCredential ? (
|
|
|
|
|
<div className="flex flex-col h-full min-h-0">
|
|
|
|
|
<CredentialEditor
|
|
|
|
|
editingCredential={editingCredential}
|
|
|
|
|
onFormSubmit={handleCredentialFormSubmit}
|
|
|
|
|
onBack={() => {
|
|
|
|
|
setEditingCredential(null);
|
|
|
|
|
setIsAddingCredential(false);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-col h-full min-h-0 overflow-auto thin-scrollbar">
|
|
|
|
|
<CredentialsManager
|
|
|
|
|
onEditCredential={handleEditCredential}
|
|
|
|
|
onAddCredential={handleAddCredential}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-12 14:42:00 -05:00
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|