feat: auto trim host inputs, fix file manager jump hosts, dashboard prevent duplicates, file manager terminal not size updating, improve left sidebar sorting, hide/show tags, add apperance user profile tab, add new host manager tabs.

This commit is contained in:
LukeGus
2025-12-22 02:41:20 -06:00
parent a73f767072
commit c27280d037
9 changed files with 422 additions and 187 deletions

View File

@@ -616,23 +616,35 @@ export function Dashboard({
{t("dashboard.noRecentActivity")}
</p>
) : (
recentActivity.map((item) => (
<Button
key={item.id}
variant="outline"
className="border-2 !border-dark-border bg-dark-bg min-w-0"
onClick={() => handleActivityClick(item)}
>
{item.type === "terminal" ? (
<Terminal size={20} className="shrink-0" />
) : (
<FolderOpen size={20} className="shrink-0" />
)}
<p className="truncate ml-2 font-semibold">
{item.hostName}
</p>
</Button>
))
recentActivity
.filter((item, index, array) => {
// Always show the first item
if (index === 0) return true;
// Show if different from previous item (by hostId and type)
const prevItem = array[index - 1];
return !(
item.hostId === prevItem.hostId &&
item.type === prevItem.type
);
})
.map((item) => (
<Button
key={item.id}
variant="outline"
className="border-2 !border-dark-border bg-dark-bg min-w-0"
onClick={() => handleActivityClick(item)}
>
{item.type === "terminal" ? (
<Terminal size={20} className="shrink-0" />
) : (
<FolderOpen size={20} className="shrink-0" />
)}
<p className="truncate ml-2 font-semibold">
{item.hostName}
</p>
</Button>
))
)}
</div>
</div>

View File

@@ -336,6 +336,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
credentialId: currentHost.credentialId,
userId: currentHost.userId,
forceKeyboardInteractive: currentHost.forceKeyboardInteractive,
jumpHosts: currentHost.jumpHosts,
useSocks5: currentHost.useSocks5,
socks5Host: currentHost.socks5Host,
socks5Port: currentHost.socks5Port,
@@ -774,6 +775,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
sshKey: currentHost.key,
keyPassword: currentHost.keyPassword,
credentialId: currentHost.credentialId,
jumpHosts: currentHost.jumpHosts,
useSocks5: currentHost.useSocks5,
socks5Host: currentHost.socks5Host,
socks5Port: currentHost.socks5Port,
@@ -1325,6 +1327,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
authType: currentHost.authType,
credentialId: currentHost.credentialId,
userId: currentHost.userId,
jumpHosts: currentHost.jumpHosts,
useSocks5: currentHost.useSocks5,
socks5Host: currentHost.socks5Host,
socks5Port: currentHost.socks5Port,
@@ -1482,6 +1485,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
authType: credentials.password ? "password" : "key",
credentialId: currentHost.credentialId,
userId: currentHost.userId,
jumpHosts: currentHost.jumpHosts,
useSocks5: currentHost.useSocks5,
socks5Host: currentHost.socks5Host,
socks5Port: currentHost.socks5Port,

View File

@@ -60,6 +60,15 @@ export function TerminalWindow({
const handleMaximize = () => {
maximizeWindow(windowId);
// Trigger resize after maximize/restore
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = setTimeout(() => {
if (terminalRef.current?.fit) {
terminalRef.current.fit();
}
}, 150);
};
const handleFocus = () => {

View File

@@ -219,6 +219,7 @@ function QuickActionItem({
placeholder={t("hosts.quickActionName")}
value={quickAction.name}
onChange={(e) => onUpdate(e.target.value, quickAction.snippetId)}
onBlur={(e) => onUpdate(e.target.value.trim(), quickAction.snippetId)}
className="flex-1"
/>
</div>
@@ -1196,6 +1197,10 @@ export function HostManagerEditor({
field.ref(e);
ipInputRef.current = e;
}}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
@@ -1238,6 +1243,10 @@ export function HostManagerEditor({
placeholder={t("placeholders.username")}
disabled={shouldDisable}
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
@@ -1259,6 +1268,10 @@ export function HostManagerEditor({
<Input
placeholder={t("placeholders.hostname")}
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
@@ -1283,6 +1296,10 @@ export function HostManagerEditor({
field.onChange(e);
setFolderDropdownOpen(true);
}}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
{folderDropdownOpen && filteredFolders.length > 0 && (
@@ -1878,6 +1895,10 @@ export function HostManagerEditor({
<Input
placeholder="proxy.example.com"
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
<FormDescription>
@@ -1927,6 +1948,10 @@ export function HostManagerEditor({
<Input
placeholder={t("hosts.username")}
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
@@ -2047,6 +2072,23 @@ export function HostManagerEditor({
newChain,
);
}}
onBlur={(e) => {
const currentChain =
form.watch(
"socks5ProxyChain",
) || [];
const newChain = [
...currentChain,
];
newChain[index] = {
...newChain[index],
host: e.target.value.trim(),
};
form.setValue(
"socks5ProxyChain",
newChain,
);
}}
/>
</div>
@@ -2142,6 +2184,23 @@ export function HostManagerEditor({
newChain,
);
}}
onBlur={(e) => {
const currentChain =
form.watch(
"socks5ProxyChain",
) || [];
const newChain = [
...currentChain,
];
newChain[index] = {
...newChain[index],
username: e.target.value.trim(),
};
form.setValue(
"socks5ProxyChain",
newChain,
);
}}
/>
</div>
@@ -2694,6 +2753,10 @@ export function HostManagerEditor({
<Input
placeholder="mosh user@server"
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
<FormDescription>
@@ -2769,6 +2832,10 @@ export function HostManagerEditor({
<Input
placeholder="Variable name"
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
@@ -2780,7 +2847,14 @@ export function HostManagerEditor({
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input placeholder="Value" {...field} />
<Input
placeholder="Value"
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
</FormItem>
)}
@@ -3057,6 +3131,10 @@ export function HostManagerEditor({
}),
);
}}
onBlur={(e) => {
endpointHostField.onChange(e.target.value.trim());
endpointHostField.onBlur();
}}
/>
</FormControl>
{sshConfigDropdownOpen[index] &&
@@ -3244,6 +3322,10 @@ export function HostManagerEditor({
<Input
placeholder={t("placeholders.homePath")}
{...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
}}
/>
</FormControl>
<FormDescription>

View File

@@ -48,8 +48,6 @@ import {
Pencil,
FolderMinus,
Copy,
Activity,
Clock,
Palette,
Trash,
Cloud,
@@ -63,6 +61,8 @@ import {
FolderOpen,
Share2,
Users,
ArrowDownUp,
Container,
} from "lucide-react";
import type {
SSHHost,
@@ -583,46 +583,6 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
}
};
const getMonitoringStatus = (host: SSHHost) => {
try {
const statsConfig = host.statsConfig
? JSON.parse(host.statsConfig)
: DEFAULT_STATS_CONFIG;
const formatInterval = (seconds: number): string => {
if (seconds >= 60) {
const minutes = Math.round(seconds / 60);
return `${minutes}m`;
}
return `${seconds}s`;
};
const statusEnabled = statsConfig.statusCheckEnabled !== false;
const metricsEnabled = statsConfig.metricsEnabled !== false;
const statusInterval = statusEnabled
? formatInterval(statsConfig.statusCheckInterval || 30)
: null;
const metricsInterval = metricsEnabled
? formatInterval(statsConfig.metricsInterval || 30)
: null;
return {
statusEnabled,
metricsEnabled,
statusInterval,
metricsInterval,
bothDisabled: !statusEnabled && !metricsEnabled,
};
} catch {
return {
statusEnabled: true,
metricsEnabled: true,
statusInterval: "30s",
metricsInterval: "30s",
bothDisabled: false,
};
}
};
const filteredAndSortedHosts = useMemo(() => {
let filtered = hosts;
@@ -1419,48 +1379,15 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
{t("hosts.fileManagerBadge")}
</Badge>
)}
{(() => {
const monitoringStatus =
getMonitoringStatus(host);
if (monitoringStatus.bothDisabled) {
return (
<Badge
variant="outline"
className="text-xs px-1 py-0 text-muted-foreground"
>
<Activity className="h-2 w-2 mr-0.5" />
{t("hosts.monitoringDisabledBadge")}
</Badge>
);
}
return (
<>
{monitoringStatus.statusEnabled && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Activity className="h-2 w-2 mr-0.5" />
{t("hosts.statusMonitoring")}:{" "}
{monitoringStatus.statusInterval}
</Badge>
)}
{monitoringStatus.metricsEnabled && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Clock className="h-2 w-2 mr-0.5" />
{t("hosts.metricsMonitoring")}:{" "}
{monitoringStatus.metricsInterval}
</Badge>
)}
</>
);
})()}
{host.enableDocker && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Container className="h-2 w-2 mr-0.5" />
Docker
</Badge>
)}
</div>
</div>
@@ -1519,6 +1446,60 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</TooltipContent>
</Tooltip>
)}
{host.enableTunnel && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "tunnel",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-orange-500/10 hover:border-orange-500/50 flex-1"
>
<ArrowDownUp className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Tunnels</p>
</TooltipContent>
</Tooltip>
)}
{host.enableDocker && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
addTab({
type: "docker",
title,
hostConfig: host,
});
}}
className="h-7 px-2 hover:bg-cyan-500/10 hover:border-cyan-500/50 flex-1"
>
<Container className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Docker</p>
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button