fix: More bug fixes and QOL fixes
This commit is contained in:
@@ -15,6 +15,7 @@ import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -52,6 +53,8 @@ export function CredentialEditor({
|
||||
const [detectedKeyType, setDetectedKeyType] = useState<string | null>(null);
|
||||
const [keyDetectionLoading, setKeyDetectionLoading] = useState(false);
|
||||
const keyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("general");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<
|
||||
string | null
|
||||
@@ -60,6 +63,11 @@ export function CredentialEditor({
|
||||
useState(false);
|
||||
const publicKeyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Clear error when tab changes
|
||||
useEffect(() => {
|
||||
setFormError(null);
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -320,6 +328,8 @@ export function CredentialEditor({
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
setFormError(null);
|
||||
|
||||
if (!data.name || data.name.trim() === "") {
|
||||
data.name = data.username;
|
||||
}
|
||||
@@ -378,6 +388,28 @@ export function CredentialEditor({
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormError = () => {
|
||||
const errors = form.formState.errors;
|
||||
|
||||
if (
|
||||
errors.name ||
|
||||
errors.username ||
|
||||
errors.description ||
|
||||
errors.folder ||
|
||||
errors.tags
|
||||
) {
|
||||
setActiveTab("general");
|
||||
} else if (
|
||||
errors.password ||
|
||||
errors.key ||
|
||||
errors.publicKey ||
|
||||
errors.keyPassword ||
|
||||
errors.keyType
|
||||
) {
|
||||
setActiveTab("authentication");
|
||||
}
|
||||
};
|
||||
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
|
||||
const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
|
||||
@@ -427,11 +459,20 @@ export function CredentialEditor({
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
onSubmit={form.handleSubmit(onSubmit, handleFormError)}
|
||||
className="flex flex-col flex-1 min-h-0 h-full"
|
||||
>
|
||||
<ScrollArea className="flex-1 min-h-0 w-full my-1 pb-2">
|
||||
<Tabs defaultValue="general" className="w-full">
|
||||
{formError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>{formError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
{t("credentials.general")}
|
||||
|
||||
@@ -204,27 +204,59 @@ export function Dashboard({
|
||||
|
||||
setServerStatsLoading(true);
|
||||
const serversWithStats = await Promise.all(
|
||||
hosts.slice(0, 50).map(async (host: { id: number; name: string }) => {
|
||||
try {
|
||||
const metrics = await getServerMetricsById(host.id);
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name || `Host ${host.id}`,
|
||||
cpu: metrics.cpu.percent,
|
||||
ram: metrics.memory.percent,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name || `Host ${host.id}`,
|
||||
cpu: null,
|
||||
ram: null,
|
||||
};
|
||||
}
|
||||
}),
|
||||
hosts
|
||||
.slice(0, 50)
|
||||
.map(
|
||||
async (host: {
|
||||
id: number;
|
||||
name: string;
|
||||
statsConfig?: string | { metricsEnabled?: boolean };
|
||||
}) => {
|
||||
try {
|
||||
// Parse statsConfig if it's a string
|
||||
let statsConfig: { metricsEnabled?: boolean } = {
|
||||
metricsEnabled: true,
|
||||
};
|
||||
if (host.statsConfig) {
|
||||
if (typeof host.statsConfig === "string") {
|
||||
statsConfig = JSON.parse(host.statsConfig);
|
||||
} else {
|
||||
statsConfig = host.statsConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if metrics are disabled
|
||||
if (statsConfig.metricsEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metrics = await getServerMetricsById(host.id);
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name || `Host ${host.id}`,
|
||||
cpu: metrics.cpu.percent,
|
||||
ram: metrics.memory.percent,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name || `Host ${host.id}`,
|
||||
cpu: null,
|
||||
ram: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
const validServerStats = serversWithStats.filter(
|
||||
(server) => server.cpu !== null && server.ram !== null,
|
||||
(
|
||||
server,
|
||||
): server is {
|
||||
id: number;
|
||||
name: string;
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
} => server !== null && server.cpu !== null && server.ram !== null,
|
||||
);
|
||||
setServerStats(validServerStats);
|
||||
setServerStatsLoading(false);
|
||||
@@ -339,7 +371,7 @@ export function Dashboard({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex"
|
||||
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex min-w-0"
|
||||
style={{
|
||||
marginLeft: leftMarginPx,
|
||||
marginRight: rightSidebarOpen
|
||||
@@ -352,19 +384,19 @@ export function Dashboard({
|
||||
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col relative z-10 w-full h-full">
|
||||
<div className="flex flex-row items-center justify-between w-full px-3 mt-3">
|
||||
<div className="text-2xl text-white font-semibold">
|
||||
<div className="flex flex-col relative z-10 w-full h-full min-w-0">
|
||||
<div className="flex flex-row items-center justify-between w-full px-3 mt-3 min-w-0 flex-wrap gap-2">
|
||||
<div className="text-2xl text-white font-semibold shrink-0">
|
||||
{t("dashboard.title")}
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<div className="flex flex-col items-center gap-4 justify-center mr-5">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<div className="flex flex-row gap-3 flex-wrap min-w-0">
|
||||
<div className="flex flex-col items-center gap-4 justify-center mr-5 min-w-0 shrink">
|
||||
<p className="text-muted-foreground text-sm whitespace-nowrap">
|
||||
Press <Kbd>LShift</Kbd> twice to open the command palette
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="font-semibold"
|
||||
className="font-semibold shrink-0"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
@@ -376,7 +408,7 @@ export function Dashboard({
|
||||
{t("dashboard.github")}
|
||||
</Button>
|
||||
<Button
|
||||
className="font-semibold"
|
||||
className="font-semibold shrink-0"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
@@ -388,7 +420,7 @@ export function Dashboard({
|
||||
{t("dashboard.support")}
|
||||
</Button>
|
||||
<Button
|
||||
className="font-semibold"
|
||||
className="font-semibold shrink-0"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
@@ -400,7 +432,7 @@ export function Dashboard({
|
||||
{t("dashboard.discord")}
|
||||
</Button>
|
||||
<Button
|
||||
className="font-semibold"
|
||||
className="font-semibold shrink-0"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/sponsors/LukeGus", "_blank")
|
||||
@@ -413,23 +445,23 @@ export function Dashboard({
|
||||
|
||||
<Separator className="mt-3 p-0.25" />
|
||||
|
||||
<div className="flex flex-col flex-1 my-5 mx-5 gap-4 min-h-0">
|
||||
<div className="flex flex-row flex-1 gap-4 min-h-0">
|
||||
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden">
|
||||
<div className="flex flex-col flex-1 my-5 mx-5 gap-4 min-h-0 min-w-0">
|
||||
<div className="flex flex-row flex-1 gap-4 min-h-0 min-w-0">
|
||||
<div className="flex-1 min-w-0 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden">
|
||||
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden">
|
||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||
<Server className="mr-3" />
|
||||
{t("dashboard.serverOverview")}
|
||||
</p>
|
||||
<div className="bg-dark-bg w-full h-auto border-2 border-dark-border rounded-md px-3 py-3">
|
||||
<div className="flex flex-row items-center justify-between mb-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between mb-3 min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<History
|
||||
size={20}
|
||||
color="#FFFFFF"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<p className="ml-2 leading-none">
|
||||
<p className="ml-2 leading-none truncate">
|
||||
{t("dashboard.version")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -451,14 +483,14 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between mb-5">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between mb-5 min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<Clock
|
||||
size={20}
|
||||
color="#FFFFFF"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<p className="ml-2 leading-none">
|
||||
<p className="ml-2 leading-none truncate">
|
||||
{t("dashboard.uptime")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -470,14 +502,14 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<Database
|
||||
size={20}
|
||||
color="#FFFFFF"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<p className="ml-2 leading-none">
|
||||
<p className="ml-2 leading-none truncate">
|
||||
{t("dashboard.database")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -494,14 +526,14 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3 min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<Server
|
||||
size={16}
|
||||
color="#FFFFFF"
|
||||
className="mr-3 shrink-0"
|
||||
/>
|
||||
<p className="m-0 leading-none">
|
||||
<p className="m-0 leading-none truncate">
|
||||
{t("dashboard.totalServers")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -509,14 +541,14 @@ export function Dashboard({
|
||||
{totalServers}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3 min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<Network
|
||||
size={16}
|
||||
color="#FFFFFF"
|
||||
className="mr-3 shrink-0"
|
||||
/>
|
||||
<p className="m-0 leading-none">
|
||||
<p className="m-0 leading-none truncate">
|
||||
{t("dashboard.totalTunnels")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -526,14 +558,14 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-between bg-dark-bg w-full h-auto mt-3 border-2 border-dark-border rounded-md px-3 py-3 min-w-0 gap-2">
|
||||
<div className="flex flex-row items-center min-w-0">
|
||||
<Key
|
||||
size={16}
|
||||
color="#FFFFFF"
|
||||
className="mr-3 shrink-0"
|
||||
/>
|
||||
<p className="m-0 leading-none">
|
||||
<p className="m-0 leading-none truncate">
|
||||
{t("dashboard.totalCredentials")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -544,7 +576,7 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex-1 min-w-0 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-between mb-3 mt-1">
|
||||
<p className="text-xl font-semibold flex flex-row items-center">
|
||||
@@ -561,7 +593,7 @@ export function Dashboard({
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={`grid gap-4 grid-cols-[repeat(auto-fit,minmax(200px,1fr))] auto-rows-min overflow-x-hidden ${recentActivityLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden ${recentActivityLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||
>
|
||||
{recentActivityLoading ? (
|
||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
||||
@@ -577,7 +609,7 @@ export function Dashboard({
|
||||
<Button
|
||||
key={item.id}
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border bg-dark-bg"
|
||||
className="border-2 !border-dark-border bg-dark-bg min-w-0"
|
||||
onClick={() => handleActivityClick(item)}
|
||||
>
|
||||
{item.type === "terminal" ? (
|
||||
@@ -595,17 +627,17 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row flex-1 gap-4 min-h-0">
|
||||
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex flex-row flex-1 gap-4 min-h-0 min-w-0">
|
||||
<div className="flex-1 min-w-0 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden">
|
||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||
<FastForward className="mr-3" />
|
||||
{t("dashboard.quickActions")}
|
||||
</p>
|
||||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(200px,1fr))] auto-rows-min overflow-y-auto overflow-x-hidden">
|
||||
<div className="grid gap-4 grid-cols-3 auto-rows-min overflow-y-auto overflow-x-hidden">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3 min-w-0"
|
||||
onClick={handleAddHost}
|
||||
>
|
||||
<Server
|
||||
@@ -618,7 +650,7 @@ export function Dashboard({
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3 min-w-0"
|
||||
onClick={handleAddCredential}
|
||||
>
|
||||
<Key
|
||||
@@ -632,7 +664,7 @@ export function Dashboard({
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3 min-w-0"
|
||||
onClick={handleOpenAdminSettings}
|
||||
>
|
||||
<Settings
|
||||
@@ -646,7 +678,7 @@ export function Dashboard({
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3 min-w-0"
|
||||
onClick={handleOpenUserProfile}
|
||||
>
|
||||
<User
|
||||
@@ -660,14 +692,14 @@ export function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex-1 min-w-0 border-2 border-dark-border rounded-md bg-dark-bg-darker flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||
<ChartLine className="mr-3" />
|
||||
{t("dashboard.serverStats")}
|
||||
</p>
|
||||
<div
|
||||
className={`grid gap-4 grid-cols-[repeat(auto-fit,minmax(200px,1fr))] auto-rows-min overflow-x-hidden ${serverStatsLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden ${serverStatsLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||
>
|
||||
{serverStatsLoading ? (
|
||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
||||
@@ -683,7 +715,7 @@ export function Dashboard({
|
||||
<Button
|
||||
key={server.id}
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border bg-dark-bg h-auto p-3"
|
||||
className="border-2 !border-dark-border bg-dark-bg h-auto p-3 min-w-0"
|
||||
>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-row items-center mb-2">
|
||||
|
||||
@@ -900,6 +900,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function handleCopyPath(files: FileItem[]) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
const paths = files.map((file) => file.path).join("\n");
|
||||
|
||||
navigator.clipboard.writeText(paths).then(
|
||||
() => {
|
||||
toast.success(
|
||||
files.length === 1
|
||||
? t("fileManager.pathCopiedToClipboard")
|
||||
: t("fileManager.pathsCopiedToClipboard", { count: files.length }),
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
console.error("Failed to copy path to clipboard:", err);
|
||||
toast.error(t("fileManager.failedToCopyPath"));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handlePasteFiles() {
|
||||
if (!clipboard || !sshSessionId) return;
|
||||
|
||||
@@ -2064,6 +2084,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
onProperties={handleOpenPermissionsDialog}
|
||||
onExtractArchive={handleExtractArchive}
|
||||
onCompress={handleOpenCompressDialog}
|
||||
onCopyPath={handleCopyPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,7 @@ interface ContextMenuProps {
|
||||
currentPath?: string;
|
||||
onExtractArchive?: (file: FileItem) => void;
|
||||
onCompress?: (files: FileItem[]) => void;
|
||||
onCopyPath?: (files: FileItem[]) => void;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
@@ -104,6 +105,7 @@ export function FileManagerContextMenu({
|
||||
currentPath,
|
||||
onExtractArchive,
|
||||
onCompress,
|
||||
onCopyPath,
|
||||
}: ContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const [menuPosition, setMenuPosition] = useState({ x, y });
|
||||
@@ -365,7 +367,18 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
if ((isSingleFile && onRename) || onCopy || onCut) {
|
||||
if (onCopyPath) {
|
||||
menuItems.push({
|
||||
icon: <Clipboard className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.copyPaths")
|
||||
: t("fileManager.copyPath"),
|
||||
action: () => onCopyPath(files),
|
||||
shortcut: "Ctrl+Shift+P",
|
||||
});
|
||||
}
|
||||
|
||||
if ((isSingleFile && onRename) || onCopy || onCut || onCopyPath) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,13 +72,14 @@ export function HostManager({
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
if (value !== "add_host") {
|
||||
// Only clear editing state when leaving the respective tabs, not when entering them
|
||||
if (activeTab === "add_host" && value !== "add_host") {
|
||||
setEditingHost(null);
|
||||
}
|
||||
if (value !== "add_credential") {
|
||||
if (activeTab === "add_credential" && value !== "add_credential") {
|
||||
setEditingCredential(null);
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||
|
||||
@@ -345,6 +345,13 @@ export function HostManagerEditor({
|
||||
"upload",
|
||||
);
|
||||
const isSubmittingRef = useRef(false);
|
||||
const [activeTab, setActiveTab] = useState("general");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
// Clear error when tab changes
|
||||
useEffect(() => {
|
||||
setFormError(null);
|
||||
}, [activeTab]);
|
||||
|
||||
const [statusIntervalUnit, setStatusIntervalUnit] = useState<
|
||||
"seconds" | "minutes"
|
||||
@@ -817,6 +824,7 @@ export function HostManagerEditor({
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
isSubmittingRef.current = true;
|
||||
setFormError(null);
|
||||
|
||||
if (!data.name || data.name.trim() === "") {
|
||||
data.name = `${data.username}@${data.ip}`;
|
||||
@@ -828,12 +836,16 @@ export function HostManagerEditor({
|
||||
|
||||
if (statusInterval < 5 || statusInterval > 3600) {
|
||||
toast.error(t("hosts.intervalValidation"));
|
||||
setActiveTab("statistics");
|
||||
setFormError(t("hosts.intervalValidation"));
|
||||
isSubmittingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (metricsInterval < 5 || metricsInterval > 3600) {
|
||||
toast.error(t("hosts.intervalValidation"));
|
||||
setActiveTab("statistics");
|
||||
setFormError(t("hosts.intervalValidation"));
|
||||
isSubmittingRef.current = false;
|
||||
return;
|
||||
}
|
||||
@@ -943,13 +955,47 @@ export function HostManagerEditor({
|
||||
);
|
||||
notifyHostCreatedOrUpdated(savedHost.id);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
toast.error(t("hosts.failedToSaveHost"));
|
||||
console.error("Failed to save host:", error);
|
||||
} finally {
|
||||
isSubmittingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form validation errors
|
||||
const handleFormError = () => {
|
||||
const errors = form.formState.errors;
|
||||
|
||||
// Determine which tab contains the error
|
||||
if (
|
||||
errors.ip ||
|
||||
errors.port ||
|
||||
errors.username ||
|
||||
errors.name ||
|
||||
errors.folder ||
|
||||
errors.tags ||
|
||||
errors.pin ||
|
||||
errors.password ||
|
||||
errors.key ||
|
||||
errors.keyPassword ||
|
||||
errors.keyType ||
|
||||
errors.credentialId ||
|
||||
errors.forceKeyboardInteractive ||
|
||||
errors.jumpHosts
|
||||
) {
|
||||
setActiveTab("general");
|
||||
} else if (errors.enableTerminal || errors.terminalConfig) {
|
||||
setActiveTab("terminal");
|
||||
} else if (errors.enableTunnel || errors.tunnelConnections) {
|
||||
setActiveTab("tunnel");
|
||||
} else if (errors.enableFileManager || errors.defaultPath) {
|
||||
setActiveTab("file_manager");
|
||||
} else if (errors.statsConfig) {
|
||||
setActiveTab("statistics");
|
||||
}
|
||||
};
|
||||
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
|
||||
const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
|
||||
@@ -1038,12 +1084,26 @@ export function HostManagerEditor({
|
||||
const getFilteredSshConfigs = (index: number) => {
|
||||
const value = form.watch(`tunnelConnections.${index}.endpointHost`);
|
||||
|
||||
const currentHostName =
|
||||
form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`;
|
||||
const currentHostId = editingHost?.id;
|
||||
|
||||
let filtered = sshConfigurations.filter(
|
||||
(config) => config !== currentHostName,
|
||||
);
|
||||
let filtered = sshConfigurations;
|
||||
|
||||
// Filter out the current host being edited (by ID, not by name)
|
||||
if (currentHostId) {
|
||||
const currentHostName = hosts.find((h) => h.id === currentHostId)?.name;
|
||||
if (currentHostName) {
|
||||
filtered = sshConfigurations.filter(
|
||||
(config) => config !== currentHostName,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If creating a new host, filter by the name being entered
|
||||
const currentHostName =
|
||||
form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`;
|
||||
filtered = sshConfigurations.filter(
|
||||
(config) => config !== currentHostName,
|
||||
);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
filtered = filtered.filter((config) =>
|
||||
@@ -1099,12 +1159,21 @@ export function HostManagerEditor({
|
||||
<div className="flex-1 flex flex-col h-full min-h-0 w-full">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
onSubmit={form.handleSubmit(onSubmit, handleFormError)}
|
||||
className="flex flex-col flex-1 min-h-0 h-full"
|
||||
>
|
||||
<ScrollArea className="flex-1 min-h-0 w-full my-1 pb-2">
|
||||
<div className="pr-4">
|
||||
<Tabs defaultValue="general" className="w-full">
|
||||
{formError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>{formError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
{t("hosts.general")}
|
||||
|
||||
@@ -199,9 +199,6 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
await fetchHosts();
|
||||
await fetchFolderMetadata();
|
||||
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
||||
|
||||
const { refreshServerPolling } = await import("@/ui/main-axios.ts");
|
||||
refreshServerPolling();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete hosts in folder:", error);
|
||||
toast.error(t("hosts.failedToDeleteHostsInFolder"));
|
||||
|
||||
Reference in New Issue
Block a user