diff --git a/src/components/JobDetailsDrawer.tsx b/src/components/JobDetailsDrawer.tsx index 615dc11..ab97e65 100644 --- a/src/components/JobDetailsDrawer.tsx +++ b/src/components/JobDetailsDrawer.tsx @@ -14,10 +14,11 @@ import { import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { message } from '@tauri-apps/plugin-dialog' import { platform } from '@tauri-apps/plugin-os' -import { SearchCheckIcon, SquareIcon } from 'lucide-react' -import { useMemo } from 'react' +import { ExternalLinkIcon, RefreshCwIcon, SearchCheckIcon, SquareIcon } from 'lucide-react' +import { useMemo, useState } from 'react' import { formatBytes } from '../../lib/format' import notify from '../../lib/notify' +import { startBatch } from '../../lib/rclone/api' import rclone from '../../lib/rclone/client' import type { JobItem } from '../../types/jobs' @@ -25,12 +26,17 @@ export default function JobDetailsDrawer({ isOpen, onClose, selectedJob, + onSelectJob, }: { isOpen: boolean onClose: () => void selectedJob: JobItem + onSelectJob?: (job: JobItem) => void }) { const queryClient = useQueryClient() + const [retryStatus, setRetryStatus] = useState< + Map + >(new Map()) const jobStatusQuery = useQuery({ queryKey: ['jobs', 'status', selectedJob.id], @@ -118,6 +124,30 @@ export default function JobDetailsDrawer({ }, }) + const retryMutation = useMutation({ + mutationFn: async ({ + key, + input, + }: { key: string; input: { _path: string } & Record }) => { + setRetryStatus((prev) => new Map(prev).set(key, { status: 'pending' })) + const jobId = await startBatch([input]) + return { key, jobId } + }, + onSuccess: ({ key, jobId }) => { + setRetryStatus((prev) => new Map(prev).set(key, { status: 'started', jobId })) + queryClient.refetchQueries({ queryKey: ['transfers', 'list', 'all'] }) + }, + onError: (error, { key }) => { + console.error('Failed to retry transfer:', error) + setRetryStatus((prev) => + new Map(prev).set(key, { + status: 'error', + error: error instanceof Error ? error.message : 'Unknown error', + }) + ) + }, + }) + const transferred = useMemo( () => transferredQuery.data?.transferred || [], [transferredQuery.data] @@ -131,6 +161,17 @@ export default function JobDetailsDrawer({ [jobGroupStatsQuery.data] ) + const errorInputMap = useMemo(() => { + const map = new Map>() + for (const err of otherErrors) { + const input = err.input as { srcRemote?: string; _path: string } & Record + if (input?.srcRemote) { + map.set(input.srcRemote, input) + } + } + return map + }, [otherErrors]) + return ( { + const itemKey = item.name || `item-${itemIndex}` + const itemRetryStatus = retryStatus.get(itemKey) + const inputForRetry = item.name + ? errorInputMap.get(item.name) + : undefined + const canRetry = + item.error && inputForRetry && !itemRetryStatus?.status + return (

{item.name}

- {item.error ? ( -

{item.error}

- ) : item.completed_at ? ( -

{new Date(item.completed_at).toLocaleString()}

- ) : null} - {/*

{item.what}

*/} + + {itemRetryStatus?.status === 'pending' && ( +
+ + Retrying... +
+ )} + + {itemRetryStatus?.status === 'started' && + itemRetryStatus.jobId && ( +
+ + Job #{itemRetryStatus.jobId} + + {onSelectJob && ( + + + + )} +
+ )} + + {itemRetryStatus?.status === 'error' && ( +
+ + {itemRetryStatus.error || 'Retry failed'} + + {inputForRetry && ( + + + + )} +
+ )} + + {!itemRetryStatus && item.error && ( +
+ + {item.error} + + {canRetry && inputForRetry && ( + + + + )} +
+ )} + + {!itemRetryStatus && !item.error && item.completed_at && ( +

+ {new Date(item.completed_at).toLocaleString()} +

+ )}
) })} diff --git a/src/pages/Transfers.tsx b/src/pages/Transfers.tsx index 4549b20..5885aea 100644 --- a/src/pages/Transfers.tsx +++ b/src/pages/Transfers.tsx @@ -120,7 +120,12 @@ export default function Transfers() { /> {selectedJob && ( - + )} )