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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
b0d0e3a5f3
commit
883b240f73
2 files changed
+50
-74
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
* Right drawer for SSH port forwarding (SessionDialog-style).
|
* Right drawer for SSH port forwarding (SessionDialog-style).
|
||||||
* Portaled to document.body so TabContent overflow-hidden cannot clip it.
|
* 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.
|
* Local / Remote rules appear in the list and can be added repeatedly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -49,8 +49,6 @@ interface ForwardRuleDrawerProps {
|
|||||||
onStartRule?: (rule: ForwardRule) => void
|
onStartRule?: (rule: ForwardRule) => void
|
||||||
onStopRule?: (rule: ForwardRule) => void
|
onStopRule?: (rule: ForwardRule) => void
|
||||||
onDeleteRule?: (rule: ForwardRule) => void
|
onDeleteRule?: (rule: ForwardRule) => void
|
||||||
/** Persist dynamic listen port while stopped (list mode). */
|
|
||||||
onDynamicPortChange?: (port: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_DRAFT: ForwardRuleDraft = {
|
const EMPTY_DRAFT: ForwardRuleDraft = {
|
||||||
@@ -103,7 +101,6 @@ export default function ForwardRuleDrawer({
|
|||||||
onStartRule,
|
onStartRule,
|
||||||
onStopRule,
|
onStopRule,
|
||||||
onDeleteRule,
|
onDeleteRule,
|
||||||
onDynamicPortChange,
|
|
||||||
}: ForwardRuleDrawerProps) {
|
}: ForwardRuleDrawerProps) {
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
const type = (draft.type || 'local') as ForwardRuleType
|
const type = (draft.type || 'local') as ForwardRuleType
|
||||||
@@ -244,25 +241,6 @@ export default function ForwardRuleDrawer({
|
|||||||
<p className="mt-0.5 font-mono text-[11px] text-txt-muted">
|
<p className="mt-0.5 font-mono text-[11px] text-txt-muted">
|
||||||
127.0.0.1:{dynamicPort}
|
127.0.0.1:{dynamicPort}
|
||||||
</p>
|
</p>
|
||||||
{!dynamicRunning && dynamicRule && onDynamicPortChange && (
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<label className="text-[10px] text-txt-faint">{t('portForward.localListenPort')}</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={65535}
|
|
||||||
disabled={busy}
|
|
||||||
value={resolveDynamicPort(dynamicRule.localPort)}
|
|
||||||
onChange={(e) => onDynamicPortChange(Number(e.target.value))}
|
|
||||||
className="ui-input h-7 w-24 py-0 font-mono text-[11px]"
|
|
||||||
inputMode="numeric"
|
|
||||||
title={t('portForward.dynamicPortHint')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="mt-1 text-[10px] leading-relaxed text-txt-faint">
|
|
||||||
{t('portForward.dynamicSingleHint')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 gap-1">
|
<div className="flex shrink-0 gap-1">
|
||||||
{dynamicRule && dynamicRunning && onStopRule && (
|
{dynamicRule && dynamicRunning && onStopRule && (
|
||||||
|
|||||||
@@ -209,10 +209,22 @@ function RemoteHostMetricsStrip({
|
|||||||
transferLabel?: string | null
|
transferLabel?: string | null
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
const stripRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [diskSlots, setDiskSlots] = useState(1)
|
||||||
|
|
||||||
const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null
|
const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null
|
||||||
const tz = metrics ? timezoneLabel(metrics) : null
|
const tz = metrics ? timezoneLabel(metrics) : null
|
||||||
const tzTip = metrics ? timezoneTooltip(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 =
|
const memDetail =
|
||||||
metrics?.memUsedKb != null && metrics?.memTotalKb != null
|
metrics?.memUsedKb != null && metrics?.memTotalKb != null
|
||||||
? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})`
|
? ` (${formatBytes(metrics.memUsedKb * 1024)}/${formatBytes(metrics.memTotalKb * 1024)})`
|
||||||
@@ -223,30 +235,56 @@ function RemoteHostMetricsStrip({
|
|||||||
metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null,
|
metrics?.cpuPercent != null ? `CPU ${formatCpuPercent(metrics.cpuPercent)}` : null,
|
||||||
metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null,
|
metrics?.memPercent != null ? t('metrics.memory', { percent: metrics.memPercent, detail: memDetail }) : null,
|
||||||
transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null,
|
transferLabel ? t('metrics.sessionTransfer', { value: transferLabel.replace(/^↑↓\s*/, '') }) : null,
|
||||||
...disks.map(
|
...rankedDisks.map(
|
||||||
(d) =>
|
(d) =>
|
||||||
`${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
|
`${d.mount} ${d.percent}% (${formatBytes(d.usedKb * 1024)}/${formatBytes(d.totalKb * 1024)})`
|
||||||
),
|
),
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = stripRef.current
|
||||||
|
const parent = el?.parentElement
|
||||||
|
if (!el || !parent) return
|
||||||
|
|
||||||
|
const updateSlots = () => {
|
||||||
|
const spacer = parent.querySelector<HTMLElement>('[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<HTMLElement>('[data-statusbar-spacer]')
|
||||||
|
if (spacer) observer.observe(spacer)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [rankedDisks.length, osLabel, tz, transferLabel, metrics?.cpuPercent, metrics?.memPercent])
|
||||||
|
|
||||||
const hasAny =
|
const hasAny =
|
||||||
osLabel ||
|
osLabel ||
|
||||||
tz ||
|
tz ||
|
||||||
metrics?.cpuPercent != null ||
|
metrics?.cpuPercent != null ||
|
||||||
metrics?.memPercent != null ||
|
metrics?.memPercent != null ||
|
||||||
transferLabel ||
|
transferLabel ||
|
||||||
disks.length > 0
|
rankedDisks.length > 0
|
||||||
if (!hasAny) return null
|
if (!hasAny) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`ml-3 flex min-w-0 max-w-[58vw] items-center gap-2 overflow-hidden font-mono tabular-nums ${
|
ref={stripRef}
|
||||||
|
className={`ml-3 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap font-mono tabular-nums ${
|
||||||
stale ? 'opacity-60' : 'opacity-100'
|
stale ? 'opacity-60' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
title={tipParts.join(' · ')}
|
title={tipParts.join(' · ')}
|
||||||
>
|
>
|
||||||
{osLabel && (
|
{osLabel && (
|
||||||
<span className="text-content-secondary max-w-[11rem] shrink-0 truncate" title={osLabel}>
|
<span className="text-content-secondary max-w-[10rem] shrink truncate" title={osLabel}>
|
||||||
{osLabel}
|
{osLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -265,8 +303,8 @@ function RemoteHostMetricsStrip({
|
|||||||
{transferLabel}
|
{transferLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{disks.map((d) => (
|
{visibleDisks.map((d) => (
|
||||||
<span key={d.mount} className={`hidden shrink-0 lg:inline ${pctClass(d.percent)}`}>
|
<span key={d.mount} className={`shrink-0 ${pctClass(d.percent)}`}>
|
||||||
{shortMount(d.mount)} {d.percent}%
|
{shortMount(d.mount)} {d.percent}%
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -296,7 +334,7 @@ export default function StatusBar() {
|
|||||||
|
|
||||||
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId])
|
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(() => {
|
useMemo(() => {
|
||||||
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
|
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
|
||||||
return {
|
return {
|
||||||
@@ -779,18 +817,6 @@ export default function StatusBar() {
|
|||||||
toast,
|
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(
|
const handleCopySocks = useCallback(
|
||||||
async (event: { preventDefault: () => void }) => {
|
async (event: { preventDefault: () => void }) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -825,28 +851,6 @@ export default function StatusBar() {
|
|||||||
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
|
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
|
||||||
const statusLabel = t(statusCfg.labelKey)
|
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 =
|
const transferLabel =
|
||||||
stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
|
stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null
|
||||||
|
|
||||||
@@ -855,7 +859,7 @@ export default function StatusBar() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={togglePanel}
|
onClick={togglePanel}
|
||||||
className={`-ml-1 flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-0.5 transition-colors
|
className={`-ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors
|
||||||
hover:bg-dark-700/70 group
|
hover:bg-dark-700/70 group
|
||||||
${statusCfg.colorClass} ${statusCfg.animateClass}`}
|
${statusCfg.colorClass} ${statusCfg.animateClass}`}
|
||||||
title={t('common:connectionPanel.title')}
|
title={t('common:connectionPanel.title')}
|
||||||
@@ -870,11 +874,6 @@ export default function StatusBar() {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tabKindLabel && (
|
|
||||||
<span className="bg-dark-700/80 text-txt-faint ml-1.5 rounded px-1.5 py-px text-[10px] capitalize">
|
|
||||||
{tabKindLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{totalConnections > 0 && (
|
{totalConnections > 0 && (
|
||||||
<span className="bg-accent-blue/20 ml-1 rounded-full px-1.5 py-px text-[10px] font-medium tabular-nums text-accent-blue">
|
<span className="bg-accent-blue/20 ml-1 rounded-full px-1.5 py-px text-[10px] font-medium tabular-nums text-accent-blue">
|
||||||
{totalConnections}
|
{totalConnections}
|
||||||
@@ -892,7 +891,7 @@ export default function StatusBar() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div data-statusbar-spacer className="min-w-2 flex-1" />
|
||||||
|
|
||||||
{socksAvailable && (
|
{socksAvailable && (
|
||||||
<button
|
<button
|
||||||
@@ -907,7 +906,7 @@ export default function StatusBar() {
|
|||||||
}
|
}
|
||||||
aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })}
|
aria-label={t('common:statusBar.forwardOpenIdle', { port: dynamicListenPort })}
|
||||||
aria-pressed={socksRunning}
|
aria-pressed={socksRunning}
|
||||||
className={`mr-2 flex items-center gap-1 rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50
|
className={`mr-2 flex shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-1.5 py-0.5 transition-colors disabled:opacity-50
|
||||||
${
|
${
|
||||||
socksRunning
|
socksRunning
|
||||||
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30'
|
? 'bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30'
|
||||||
@@ -950,7 +949,6 @@ export default function StatusBar() {
|
|||||||
onStartRule={(rule) => void handleStartRule(rule)}
|
onStartRule={(rule) => void handleStartRule(rule)}
|
||||||
onStopRule={(rule) => void handleStopRule(rule)}
|
onStopRule={(rule) => void handleStopRule(rule)}
|
||||||
onDeleteRule={(rule) => void handleDeleteRule(rule)}
|
onDeleteRule={(rule) => void handleDeleteRule(rule)}
|
||||||
onDynamicPortChange={handleDynamicPortChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user