Files
opsxshell/src/components/sftp/TransferPanel.tsx
T
Logan.LiandCursor 08696896f9 feat: harden local shell, SFTP, and desktop session reliability
Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 16:08:11 +08:00

104 lines
3.6 KiB
TypeScript

/**
* TransferPanel - 传输队列面板
*
* 显示文件上传/下载的传输队列;进行中任务置顶。
*/
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { TransferItem } from './TransferItem'
import type { TransferTask } from '../../../shared/types/sftp.types'
interface TransferPanelProps {
tasks: TransferTask[]
isOpen: boolean
onToggle: () => void
onCancel?: (id: string) => void
onRemove: (id: string) => void
onRetry?: (id: string) => void
}
/** transferring → pending → failed/canceled → completed(组内保持原相对顺序) */
// eslint-disable-next-line react-refresh/only-export-components -- pure helper used by tests and panel
export function sortTransferTasks(tasks: TransferTask[]): TransferTask[] {
const rank = (status: TransferTask['status']): number => {
switch (status) {
case 'transferring':
return 0
case 'pending':
return 1
case 'failed':
case 'canceled':
return 2
case 'completed':
return 3
default:
return 4
}
}
return tasks
.map((task, index) => ({ task, index }))
.sort((a, b) => {
const byStatus = rank(a.task.status) - rank(b.task.status)
return byStatus !== 0 ? byStatus : a.index - b.index
})
.map(({ task }) => task)
}
export function TransferPanel({ tasks, isOpen, onToggle, onCancel, onRemove, onRetry }: TransferPanelProps) {
const { t } = useTranslation('sftp')
const orderedTasks = useMemo(() => sortTransferTasks(tasks), [tasks])
const activeCount = tasks.filter((task) => task.status === 'pending' || task.status === 'transferring').length
const completedCount = tasks.filter((task) => task.status === 'completed').length
return (
<div className="bg-dark-800 border-t border-dark-700/30">
<button
type="button"
onClick={onToggle}
className="w-full flex items-center justify-between px-4 py-2 hover:bg-dark-700 transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{t('transfer.queue')}</span>
{activeCount > 0 && (
<span className="px-2 py-0.5 bg-accent-blue/20 text-accent-blue text-xs rounded-full">
{t('transfer.active', { count: activeCount })}
</span>
)}
{completedCount > 0 && (
<span className="px-2 py-0.5 bg-feedback-success/15 text-feedback-success text-xs rounded-full">
{t('transfer.completed', { count: completedCount })}
</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-txt-muted">{t('transfer.taskCount', { count: tasks.length })}</span>
{isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
</div>
</button>
{isOpen && orderedTasks.length > 0 && (
<div className="max-h-48 overflow-y-auto border-t border-dark-700/30">
{orderedTasks.map((task) => (
<TransferItem
key={task.id}
task={task}
onCancel={onCancel ? () => onCancel(task.id) : undefined}
onRemove={() => onRemove(task.id)}
onRetry={onRetry ? () => onRetry(task.id) : undefined}
/>
))}
</div>
)}
{isOpen && tasks.length === 0 && (
<div className="py-4 text-center text-txt-muted text-sm">
{t('transfer.empty')}
</div>
)}
</div>
)
}