feat: release OpsXShell 2.1.0 native Tauri edition
This commit is contained in:
1 parent
e9a0d21758
commit
a3dba0c6ed
311 files changed
+31748
-14897
No files matched your search
@@ -0,0 +1,255 @@
|
||||
import { Wifi, WifiOff, AlertCircle, Loader2, HardDrive, Clock, ChevronUp } from 'lucide-react'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { useTabStore, type ConnectionStatus } from '../../stores/tabStore'
|
||||
import { usePoolStore } from '../../stores/connection/poolStore'
|
||||
import type { ConnectionState } from '../../stores/connection'
|
||||
import { useConnectionPanelStore } from '../../stores/connectionPanelStore'
|
||||
import { useSessionStore } from '../../stores'
|
||||
|
||||
// ============== Status Display Config ==============
|
||||
|
||||
interface StatusDisplay {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
colorClass: string
|
||||
animateClass: string
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<string, StatusDisplay> = {
|
||||
connected: {
|
||||
icon: <Wifi className="w-3 h-3" />,
|
||||
label: '已连接',
|
||||
colorClass: 'text-green-400',
|
||||
animateClass: '',
|
||||
},
|
||||
connecting: {
|
||||
icon: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
label: '连接中',
|
||||
colorClass: 'text-yellow-400',
|
||||
animateClass: 'animate-pulse',
|
||||
},
|
||||
background: {
|
||||
icon: <Wifi className="w-3 h-3" />,
|
||||
label: '后台',
|
||||
colorClass: 'text-green-400',
|
||||
animateClass: '',
|
||||
},
|
||||
suspended: {
|
||||
icon: <WifiOff className="w-3 h-3" />,
|
||||
label: '已挂起',
|
||||
colorClass: 'text-yellow-400',
|
||||
animateClass: '',
|
||||
},
|
||||
reconnecting: {
|
||||
icon: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
label: '重连中',
|
||||
colorClass: 'text-yellow-400',
|
||||
animateClass: 'animate-pulse',
|
||||
},
|
||||
error: {
|
||||
icon: <AlertCircle className="w-3 h-3" />,
|
||||
label: '连接错误',
|
||||
colorClass: 'text-red-400',
|
||||
animateClass: '',
|
||||
},
|
||||
disconnected: {
|
||||
icon: <WifiOff className="w-3 h-3" />,
|
||||
label: '未连接',
|
||||
colorClass: 'text-txt-muted',
|
||||
animateClass: '',
|
||||
},
|
||||
}
|
||||
|
||||
// ============== Helper: format bytes ==============
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const k = 1024
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
const value = bytes / Math.pow(k, i)
|
||||
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
|
||||
}
|
||||
|
||||
// ============== Helper: map tab connectionStatus to ConnectionState ==============
|
||||
|
||||
function mapTabStatusToConnectionState(status?: ConnectionStatus): ConnectionState | undefined {
|
||||
if (!status) return undefined
|
||||
// tabStore's ConnectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||
// maps directly to ConnectionState values
|
||||
return status as ConnectionState
|
||||
}
|
||||
|
||||
// ============== Component ==============
|
||||
|
||||
export default function StatusBar() {
|
||||
const togglePanel = useConnectionPanelStore((s) => s.toggle)
|
||||
const [version, setVersion] = useState('1.0.0')
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
|
||||
// Tab store
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const tabs = useTabStore((s) => s.tabs)
|
||||
|
||||
// Connection store (pool) — subscribe to connections to reactively update
|
||||
const connections = usePoolStore((s) => s.connections)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
|
||||
// Total connection count for the panel badge
|
||||
const totalConnections = connections.size
|
||||
|
||||
// Derive the active tab
|
||||
const activeTab = useMemo(
|
||||
() => tabs.find((t) => t.id === activeTabId),
|
||||
[tabs, activeTabId]
|
||||
)
|
||||
|
||||
// Determine the connection state and info for the active tab
|
||||
const { connectionState, hostLabel, stats, tabKind } = useMemo(() => {
|
||||
if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') {
|
||||
return {
|
||||
connectionState: undefined,
|
||||
hostLabel: undefined,
|
||||
stats: undefined,
|
||||
tabKind: activeTab?.type as string | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (activeTab.type === 'serial') {
|
||||
return {
|
||||
connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus),
|
||||
hostLabel: activeTab.serialConfig
|
||||
? `${activeTab.serialConfig.path} · ${activeTab.serialConfig.baudRate}`
|
||||
: activeTab.title,
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Try to get connection from pool via connectionInstance
|
||||
const instance = activeTab.connectionInstance
|
||||
if (instance?.connectionId) {
|
||||
const conn = connections.get(instance.connectionId)
|
||||
if (conn) {
|
||||
return {
|
||||
connectionState: conn.state,
|
||||
hostLabel: `${conn.config.username}@${conn.config.host}:${conn.config.port || 22}`,
|
||||
stats: conn.stats,
|
||||
tabKind: activeTab.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Session id → session store
|
||||
if (activeTab.sessionId) {
|
||||
const session = sessions.find((s) => s.id === activeTab.sessionId)
|
||||
if (session) {
|
||||
return {
|
||||
connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus),
|
||||
hostLabel: `${session.username}@${session.host}:${session.port || 22}`,
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to tab's own connectionConfig
|
||||
const state = mapTabStatusToConnectionState(activeTab.connectionStatus)
|
||||
const config = activeTab.connectionConfig
|
||||
const label = config
|
||||
? `${config.username}@${config.host}:${config.port || 22}`
|
||||
: activeTab.isLocal
|
||||
? '本地终端'
|
||||
: undefined
|
||||
|
||||
return { connectionState: state, hostLabel: label, stats: undefined, tabKind: activeTab.type }
|
||||
}, [activeTab, connections, sessions])
|
||||
|
||||
// Clock timer + version
|
||||
useEffect(() => {
|
||||
if (window.opsxApi) {
|
||||
window.opsxApi.getVersion().then((v: string) => {
|
||||
setVersion(v)
|
||||
}).catch(() => undefined)
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCurrentTime(new Date())
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve status display
|
||||
const statusKey = connectionState ?? 'disconnected'
|
||||
const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected
|
||||
|
||||
// Format transfer info if available
|
||||
const transferLabel = stats && stats.bytesTransferred > 0
|
||||
? `↑↓ ${formatBytes(stats.bytesTransferred)}`
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="h-7 bg-dark-800/95 border-t border-dark-700/40 flex items-center px-3 text-[11px] text-content-primary select-none">
|
||||
{/* Left: Connection status area — clickable to toggle ConnectionPanel */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePanel}
|
||||
className={`flex items-center gap-1.5 rounded-md px-1.5 py-0.5 -ml-1
|
||||
hover:bg-dark-700/70 transition-colors cursor-pointer group
|
||||
${statusCfg.colorClass} ${statusCfg.animateClass}`}
|
||||
title="点击打开连接管理面板"
|
||||
>
|
||||
{statusCfg.icon && (
|
||||
<span className="transition-colors duration-300">{statusCfg.icon}</span>
|
||||
)}
|
||||
<span className="transition-colors duration-300 font-medium">{statusCfg.label}</span>
|
||||
{hostLabel && (
|
||||
<>
|
||||
<span className="text-txt-faint mx-0.5">·</span>
|
||||
<span className="text-content-secondary font-mono truncate max-w-[280px]" title={hostLabel}>
|
||||
{hostLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{tabKind && tabKind !== 'welcome' && tabKind !== 'settings' && (
|
||||
<span className="ml-1.5 px-1.5 py-px rounded text-[10px] text-txt-faint bg-dark-700/80 capitalize">
|
||||
{tabKind === 'terminal' ? '终端' : tabKind === 'sftp' ? 'SFTP' : tabKind === 'portforward' ? '转发' : tabKind}
|
||||
</span>
|
||||
)}
|
||||
{totalConnections > 0 && (
|
||||
<span className="ml-1 px-1.5 py-px rounded-full bg-accent-blue/20 text-accent-blue text-[10px] font-medium tabular-nums">
|
||||
{totalConnections}
|
||||
</span>
|
||||
)}
|
||||
<ChevronUp className="w-3 h-3 text-txt-faint transition-transform duration-200 group-hover:rotate-180" />
|
||||
</button>
|
||||
|
||||
{/* Center: Transfer stats */}
|
||||
{transferLabel && (
|
||||
<div className="flex items-center gap-1 ml-3 text-txt-muted tabular-nums">
|
||||
<HardDrive className="w-3 h-3" />
|
||||
<span>{transferLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="flex items-center gap-3 text-txt-faint">
|
||||
<div className="flex items-center gap-1 tabular-nums">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
</div>
|
||||
<span className="hidden sm:inline opacity-80">OpsXShell v{version}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user