From 883b240f73006e827690f77fdbcb2c0030a574e6 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Wed, 12 Aug 2026 00:36:50 +0800 Subject: [PATCH] fix(ui): restore status bar metrics layout and simplify Dynamic list Drop the tab-kind badge, keep connection/forward labels from wrapping, and adapt disk chips by spare width; Dynamic card is start/stop only with port edited in the form. Co-authored-by: Cursor --- .../connection/ForwardRuleDrawer.tsx | 24 +---- src/components/layout/StatusBar.tsx | 100 +++++++++--------- 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/src/components/connection/ForwardRuleDrawer.tsx b/src/components/connection/ForwardRuleDrawer.tsx index 706bccb..31a54f1 100644 --- a/src/components/connection/ForwardRuleDrawer.tsx +++ b/src/components/connection/ForwardRuleDrawer.tsx @@ -2,7 +2,7 @@ * Right drawer for SSH port forwarding (SessionDialog-style). * Portaled to document.body so TabContent overflow-hidden cannot clip it. * - * Dynamic is a single SOCKS5 on 127.0.0.1 (default port 1080, editable when stopped). + * Dynamic is a single SOCKS5 on 127.0.0.1 (default port 1080; port set in the form below). * Local / Remote rules appear in the list and can be added repeatedly. */ @@ -49,8 +49,6 @@ interface ForwardRuleDrawerProps { onStartRule?: (rule: ForwardRule) => void onStopRule?: (rule: ForwardRule) => void onDeleteRule?: (rule: ForwardRule) => void - /** Persist dynamic listen port while stopped (list mode). */ - onDynamicPortChange?: (port: number) => void } const EMPTY_DRAFT: ForwardRuleDraft = { @@ -103,7 +101,6 @@ export default function ForwardRuleDrawer({ onStartRule, onStopRule, onDeleteRule, - onDynamicPortChange, }: ForwardRuleDrawerProps) { const { t } = useTranslation('common') const type = (draft.type || 'local') as ForwardRuleType @@ -244,25 +241,6 @@ export default function ForwardRuleDrawer({

127.0.0.1:{dynamicPort}

- {!dynamicRunning && dynamicRule && onDynamicPortChange && ( -
- - onDynamicPortChange(Number(e.target.value))} - className="ui-input h-7 w-24 py-0 font-mono text-[11px]" - inputMode="numeric" - title={t('portForward.dynamicPortHint')} - /> -
- )} -

- {t('portForward.dynamicSingleHint')} -

{dynamicRule && dynamicRunning && onStopRule && ( diff --git a/src/components/layout/StatusBar.tsx b/src/components/layout/StatusBar.tsx index 5e7614c..06e3027 100644 --- a/src/components/layout/StatusBar.tsx +++ b/src/components/layout/StatusBar.tsx @@ -209,10 +209,22 @@ function RemoteHostMetricsStrip({ transferLabel?: string | null }) { const { t } = useTranslation('common') + const stripRef = useRef(null) + const [diskSlots, setDiskSlots] = useState(1) + const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null const tz = metrics ? timezoneLabel(metrics) : null const tzTip = metrics ? timezoneTooltip(metrics) : null - const disks = (metrics?.disks ?? []).slice(0, 3) + const rankedDisks = useMemo( + () => + (metrics?.disks ?? []) + .slice() + .sort((a, b) => b.percent - a.percent || a.mount.localeCompare(b.mount)) + .slice(0, 5), + [metrics?.disks] + ) + const visibleDisks = rankedDisks.slice(0, diskSlots) + const memDetail = metrics?.memUsedKb != null && metrics?.memTotalKb != null ? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})` @@ -223,30 +235,56 @@ function RemoteHostMetricsStrip({ metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null, metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null, transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null, - ...disks.map( + ...rankedDisks.map( (d) => `${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})` ), ].filter(Boolean) + useEffect(() => { + const el = stripRef.current + const parent = el?.parentElement + if (!el || !parent) return + + const updateSlots = () => { + const spacer = parent.querySelector('[data-statusbar-spacer]') + const spare = spacer?.clientWidth ?? 0 + // ~70px per chip; fullscreen usually has room for top 5. + if (spare >= 360) setDiskSlots(5) + else if (spare >= 290) setDiskSlots(4) + else if (spare >= 220) setDiskSlots(3) + else if (spare >= 150) setDiskSlots(2) + else if (spare >= 72) setDiskSlots(1) + else setDiskSlots(0) + } + + updateSlots() + const observer = new ResizeObserver(updateSlots) + observer.observe(parent) + const spacer = parent.querySelector('[data-statusbar-spacer]') + if (spacer) observer.observe(spacer) + return () => observer.disconnect() + }, [rankedDisks.length, osLabel, tz, transferLabel, metrics?.cpuPercent, metrics?.memPercent]) + const hasAny = osLabel || tz || metrics?.cpuPercent != null || metrics?.memPercent != null || transferLabel || - disks.length > 0 + rankedDisks.length > 0 if (!hasAny) return null return (
{osLabel && ( - + {osLabel} )} @@ -265,8 +303,8 @@ function RemoteHostMetricsStrip({ {transferLabel} )} - {disks.map((d) => ( - + {visibleDisks.map((d) => ( + {shortMount(d.mount)} {d.percent}% ))} @@ -296,7 +334,7 @@ export default function StatusBar() { const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId]) - const { connectionState, hostLabel, stats, tabKind, connectionId, isRemoteSsh, sessionOsPretty } = + const { connectionState, hostLabel, stats, connectionId, isRemoteSsh, sessionOsPretty } = useMemo(() => { if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') { return { @@ -779,18 +817,6 @@ export default function StatusBar() { toast, ]) - const handleDynamicPortChange = useCallback( - (port: number) => { - if (forwardSaving || socksRunning) return - const nextPort = resolveDynamicPort(port) - const next = forwardRules.map((rule) => - rule.type === 'dynamic' ? { ...rule, localPort: nextPort } : rule - ) - void persistForwardRules(next) - }, - [forwardRules, forwardSaving, persistForwardRules, socksRunning] - ) - const handleCopySocks = useCallback( async (event: { preventDefault: () => void }) => { event.preventDefault() @@ -825,28 +851,6 @@ export default function StatusBar() { const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected const statusLabel = t(statusCfg.labelKey) - const tabKindLabel = useMemo(() => { - if (!tabKind || tabKind === 'welcome' || tabKind === 'settings') return null - switch (tabKind) { - case 'terminal': - return t('common:tab.kind.terminal') - case 'sftp': - return t('common:tab.kind.sftp') - case 'portforward': - return t('common:tab.kind.portforward') - case 'serial': - return t('common:tab.kind.serial') - case 'rdp': - return t('common:tab.kind.rdp') - case 'vnc': - return t('common:tab.kind.vnc') - case 'batch': - return t('common:tab.kind.batch') - default: - return tabKind - } - }, [tabKind, t]) - const transferLabel = stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null @@ -855,7 +859,7 @@ export default function StatusBar() {
)