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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user