checking job state, #141
This commit is contained in:
+58
-13
@@ -392,7 +392,13 @@ async function fetchTransferred() {
|
|||||||
return transferred
|
return transferred
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fetchTransferred>>) {
|
async function fetchJob(
|
||||||
|
jobId: number,
|
||||||
|
transferred: Awaited<ReturnType<typeof fetchTransferred>>,
|
||||||
|
checkingItems: { group?: string; name?: string; size?: number }[]
|
||||||
|
) {
|
||||||
|
console.log('[fetchJob] fetching job', jobId)
|
||||||
|
|
||||||
const job = await rclone('/core/stats', {
|
const job = await rclone('/core/stats', {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
@@ -412,20 +418,33 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
|
|||||||
console.log('[fetchJob] job status', jobId, JSON.stringify(jobStatus, null, 2))
|
console.log('[fetchJob] job status', jobId, JSON.stringify(jobStatus, null, 2))
|
||||||
|
|
||||||
let hasError = !!jobStatus?.error
|
let hasError = !!jobStatus?.error
|
||||||
|
let isDryRun = false
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hasError &&
|
|
||||||
jobStatus.output &&
|
jobStatus.output &&
|
||||||
typeof jobStatus.output === 'object' &&
|
typeof jobStatus.output === 'object' &&
|
||||||
'results' in jobStatus.output &&
|
'results' in jobStatus.output &&
|
||||||
Array.isArray(jobStatus.output.results)
|
Array.isArray(jobStatus.output.results)
|
||||||
) {
|
) {
|
||||||
hasError = jobStatus.output.results.some((result: any) => !!result?.error)
|
if (!hasError) {
|
||||||
|
hasError = jobStatus.output.results.some((result: any) => !!result?.error)
|
||||||
|
}
|
||||||
|
isDryRun = jobStatus.output.results.some((result: any) => {
|
||||||
|
const srcFs = result.input?.srcFs || ''
|
||||||
|
return srcFs.includes('dry_run') || srcFs.includes('dry-run')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jobCheckingItems = checkingItems.filter((c) => c.group === `job/${jobId}`)
|
||||||
|
const isChecking = jobCheckingItems.length > 0
|
||||||
|
const checkingCount = jobCheckingItems.length
|
||||||
|
|
||||||
|
console.log('[fetchJob] checking state', jobId, { isChecking, checkingCount })
|
||||||
|
|
||||||
const relatedItems = transferred.filter((t) => t.group === `job/${jobId}`)
|
const relatedItems = transferred.filter((t) => t.group === `job/${jobId}`)
|
||||||
if (relatedItems.length === 0) {
|
|
||||||
console.log('[fetchJob] relatedItems not found', jobId)
|
if (relatedItems.length === 0 && !isChecking) {
|
||||||
|
console.log('[fetchJob] no relatedItems and not checking', jobId)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +474,14 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isChecking && sources.size === 0) {
|
||||||
|
for (const checkItem of jobCheckingItems) {
|
||||||
|
if (checkItem.name) {
|
||||||
|
sources.add(checkItem.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sources.size === 0 && !hasError) {
|
if (sources.size === 0 && !hasError) {
|
||||||
console.log('[fetchJob] source or hasError not found', jobId)
|
console.log('[fetchJob] source or hasError not found', jobId)
|
||||||
return null
|
return null
|
||||||
@@ -467,10 +494,13 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
|
|||||||
speed: job.speed,
|
speed: job.speed,
|
||||||
|
|
||||||
done: job.bytes === job.totalBytes,
|
done: job.bytes === job.totalBytes,
|
||||||
progress: Math.round((job.bytes / job.totalBytes) * 100),
|
progress: job.totalBytes > 0 ? Math.round((job.bytes / job.totalBytes) * 100) : 0,
|
||||||
hasError: hasError,
|
hasError: hasError,
|
||||||
|
|
||||||
sources: Array.from(sources),
|
sources: Array.from(sources),
|
||||||
|
isChecking,
|
||||||
|
checkingCount,
|
||||||
|
isDryRun,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,8 +511,10 @@ export async function listTransfers() {
|
|||||||
console.log('[listTransfers] allStats', JSON.stringify(allStats, null, 2))
|
console.log('[listTransfers] allStats', JSON.stringify(allStats, null, 2))
|
||||||
|
|
||||||
const transferring = allStats?.transferring || []
|
const transferring = allStats?.transferring || []
|
||||||
|
const checking = allStats?.checking || []
|
||||||
|
|
||||||
console.log('[listTransfers] transferring count:', transferring.length)
|
console.log('[listTransfers] transferring count:', transferring.length)
|
||||||
|
console.log('[listTransfers] checking count:', checking.length)
|
||||||
|
|
||||||
const transferred = await fetchTransferred()
|
const transferred = await fetchTransferred()
|
||||||
console.log('[listTransfers] transferred count:', transferred?.length || 0)
|
console.log('[listTransfers] transferred count:', transferred?.length || 0)
|
||||||
@@ -492,19 +524,30 @@ export async function listTransfers() {
|
|||||||
inactive: [] as JobItem[],
|
inactive: [] as JobItem[],
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeJobIds = new Set(
|
const transferringJobIds = new Set(
|
||||||
transferring
|
transferring
|
||||||
?.filter((t) => t.group?.startsWith('job/'))
|
.filter((t) => t.group?.startsWith('job/'))
|
||||||
.map((t) => Number(t.group!.split('/')[1]))
|
.map((t) => Number(t.group!.split('/')[1]))
|
||||||
.sort((a, b) => a - b)
|
|
||||||
)
|
)
|
||||||
console.log('[listTransfers] activeJobIds', activeJobIds.size)
|
|
||||||
|
const checkingJobIds = new Set(
|
||||||
|
checking
|
||||||
|
.filter((c) => c.group?.startsWith('job/'))
|
||||||
|
.map((c) => Number(c.group!.split('/')[1]))
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeJobIds = new Set([...transferringJobIds, ...checkingJobIds])
|
||||||
|
const sortedActiveJobIds = Array.from(activeJobIds).sort((a, b) => a - b)
|
||||||
|
|
||||||
|
console.log('[listTransfers] transferring job IDs:', Array.from(transferringJobIds))
|
||||||
|
console.log('[listTransfers] checking job IDs:', Array.from(checkingJobIds))
|
||||||
|
console.log('[listTransfers] combined active job IDs:', sortedActiveJobIds)
|
||||||
|
|
||||||
const isWindows = platform() === 'windows'
|
const isWindows = platform() === 'windows'
|
||||||
console.log('[listTransfers] isWindows', isWindows)
|
console.log('[listTransfers] isWindows', isWindows)
|
||||||
|
|
||||||
for (const jobId of activeJobIds) {
|
for (const jobId of sortedActiveJobIds) {
|
||||||
const job = await fetchJob(jobId, transferred)
|
const job = await fetchJob(jobId, transferred, checking)
|
||||||
if (job) {
|
if (job) {
|
||||||
jobs.active.push({
|
jobs.active.push({
|
||||||
...job,
|
...job,
|
||||||
@@ -523,12 +566,14 @@ export async function listTransfers() {
|
|||||||
console.log('[listTransfers] inactive job IDs:', Array.from(inactiveJobIds))
|
console.log('[listTransfers] inactive job IDs:', Array.from(inactiveJobIds))
|
||||||
|
|
||||||
for (const jobId of inactiveJobIds) {
|
for (const jobId of inactiveJobIds) {
|
||||||
const job = await fetchJob(jobId, transferred)
|
const job = await fetchJob(jobId, transferred, checking)
|
||||||
if (job) {
|
if (job) {
|
||||||
jobs.inactive.push({
|
jobs.inactive.push({
|
||||||
...job,
|
...job,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
type: 'inactive',
|
type: 'inactive',
|
||||||
|
isChecking: false,
|
||||||
|
checkingCount: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { message } from '@tauri-apps/plugin-dialog'
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { SquareIcon } from 'lucide-react'
|
import { SearchCheckIcon, SquareIcon } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { formatBytes } from '../../lib/format'
|
import { formatBytes } from '../../lib/format'
|
||||||
import notify from '../../lib/notify'
|
import notify from '../../lib/notify'
|
||||||
@@ -126,6 +126,10 @@ export default function JobDetailsDrawer({
|
|||||||
() => jobGroupStatsQuery.data?.transferring || [],
|
() => jobGroupStatsQuery.data?.transferring || [],
|
||||||
[jobGroupStatsQuery.data]
|
[jobGroupStatsQuery.data]
|
||||||
)
|
)
|
||||||
|
const checking = useMemo(
|
||||||
|
() => jobGroupStatsQuery.data?.checking || [],
|
||||||
|
[jobGroupStatsQuery.data]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} placement="bottom" size="2xl" onClose={onClose}>
|
<Drawer isOpen={isOpen} placement="bottom" size="2xl" onClose={onClose}>
|
||||||
@@ -169,6 +173,48 @@ export default function JobDetailsDrawer({
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{selectedJob.isDryRun && (
|
||||||
|
<Alert color="warning" variant="faded">
|
||||||
|
This is a dry-run operation. No files were actually transferred.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{checking.length > 0 ? (
|
||||||
|
<div className="flex flex-col gap-2 pb-4">
|
||||||
|
<div className="flex flex-row items-center justify-between gap-2">
|
||||||
|
<h3 className="flex flex-row items-center gap-2 text-lg font-medium">
|
||||||
|
<SearchCheckIcon className="w-5 h-5 text-warning" />
|
||||||
|
Checking
|
||||||
|
</h3>
|
||||||
|
{jobGroupStatsQuery.isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Chip size="sm" color="warning" variant="flat">
|
||||||
|
{checking.length} item{checking.length === 1 ? '' : 's'}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-default-500">
|
||||||
|
Files are being verified before transfer. This can take a while for
|
||||||
|
large directories.
|
||||||
|
</p>
|
||||||
|
{checking.map((item, itemIndex) => {
|
||||||
|
const size = item.size ? formatBytes(item.size) : 'Unknown size'
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.name || itemIndex}
|
||||||
|
className="flex flex-row items-center justify-between gap-2 pb-2 border-b border-divider"
|
||||||
|
>
|
||||||
|
<p className="flex-1 line-clamp-1 min-w-80">{item.name}</p>
|
||||||
|
<p className="text-sm tabular-nums text-default-500">
|
||||||
|
{size}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-row items-center justify-between gap-2">
|
<div className="flex flex-row items-center justify-between gap-2">
|
||||||
<h3 className="text-lg font-medium">Transferring</h3>
|
<h3 className="text-lg font-medium">Transferring</h3>
|
||||||
|
|||||||
+1
-1
@@ -189,7 +189,7 @@ export default function Copy() {
|
|||||||
sources,
|
sources,
|
||||||
destination: dest,
|
destination: dest,
|
||||||
options: {
|
options: {
|
||||||
config: configOptions,
|
config: { ...configOptions, dry_run: true },
|
||||||
copy: copyOptions,
|
copy: copyOptions,
|
||||||
filter: filterOptions,
|
filter: filterOptions,
|
||||||
remotes: remoteOptions,
|
remotes: remoteOptions,
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function Delete() {
|
|||||||
sources: [sourceFs],
|
sources: [sourceFs],
|
||||||
options: {
|
options: {
|
||||||
filter: filterOptions,
|
filter: filterOptions,
|
||||||
config: configOptions,
|
config: { ...configOptions, dry_run: true },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -216,7 +216,7 @@ export default function Move() {
|
|||||||
sources,
|
sources,
|
||||||
destination: dest,
|
destination: dest,
|
||||||
options: {
|
options: {
|
||||||
config: configOptions,
|
config: { ...configOptions, dry_run: true },
|
||||||
move: moveOptions,
|
move: moveOptions,
|
||||||
filter: filterOptions,
|
filter: filterOptions,
|
||||||
remotes: remoteOptions,
|
remotes: remoteOptions,
|
||||||
|
|||||||
+1
-1
@@ -183,7 +183,7 @@ export default function Sync() {
|
|||||||
source,
|
source,
|
||||||
destination: dest,
|
destination: dest,
|
||||||
options: {
|
options: {
|
||||||
config: configOptions,
|
config: { ...configOptions, dry_run: true },
|
||||||
sync: syncOptions,
|
sync: syncOptions,
|
||||||
filter: filterOptions,
|
filter: filterOptions,
|
||||||
remotes: remoteOptions,
|
remotes: remoteOptions,
|
||||||
|
|||||||
+29
-3
@@ -2,7 +2,7 @@ import { Card, CardBody, Progress, Tab, Tabs, Tooltip, useDisclosure } from '@he
|
|||||||
import { Button, Chip, Spinner } from '@heroui/react'
|
import { Button, Chip, Spinner } from '@heroui/react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { message } from '@tauri-apps/plugin-dialog'
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
import { ChevronRightIcon, RefreshCcwIcon } from 'lucide-react'
|
import { ChevronRightIcon, RefreshCcwIcon, SearchCheckIcon } from 'lucide-react'
|
||||||
import { startTransition, useCallback, useMemo, useState } from 'react'
|
import { startTransition, useCallback, useMemo, useState } from 'react'
|
||||||
import { buildReadablePathMultiple, formatBytes } from '../../lib/format'
|
import { buildReadablePathMultiple, formatBytes } from '../../lib/format'
|
||||||
import { listTransfers } from '../../lib/rclone/api'
|
import { listTransfers } from '../../lib/rclone/api'
|
||||||
@@ -154,7 +154,7 @@ function JobCard({ job, onSelect }: { job: JobItem; onSelect: (job: JobItem) =>
|
|||||||
</Chip>
|
</Chip>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="flex flex-row items-center flex-1">
|
<div className="flex flex-row items-center flex-1 gap-2">
|
||||||
{job.hasError && <p className="text-danger">ERROR: Tap to view details.</p>}
|
{job.hasError && <p className="text-danger">ERROR: Tap to view details.</p>}
|
||||||
|
|
||||||
{!job.hasError && (
|
{!job.hasError && (
|
||||||
@@ -169,11 +169,37 @@ function JobCard({ job, onSelect }: { job: JobItem; onSelect: (job: JobItem) =>
|
|||||||
) : null
|
) : null
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{job.isDryRun && (
|
||||||
|
<Chip size="sm" variant="flat" color="warning">
|
||||||
|
DRY RUN
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{job.type === 'active' && job.isChecking ? (
|
||||||
|
<Tooltip
|
||||||
|
content={`Checking ${job.checkingCount} file${job.checkingCount === 1 ? '' : 's'} before transfer`}
|
||||||
|
color="foreground"
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
color="warning"
|
||||||
|
startContent={<SearchCheckIcon className="w-3 h-3" />}
|
||||||
|
>
|
||||||
|
Checking {job.checkingCount}
|
||||||
|
</Chip>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{job.type === 'active' ? (
|
{job.type === 'active' ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`${formatBytes(job.bytes)} out of ${formatBytes(job.totalBytes)}`}
|
content={`${formatBytes(job.bytes)} out of ${formatBytes(job.totalBytes)}`}
|
||||||
>
|
>
|
||||||
<Progress value={job.progress} isStriped={true} />
|
<Progress
|
||||||
|
value={job.progress}
|
||||||
|
isStriped={true}
|
||||||
|
isIndeterminate={job.isChecking && job.totalBytes === 0}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Vendored
+3
@@ -8,4 +8,7 @@ export type JobItem = {
|
|||||||
progress: number
|
progress: number
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
sources: string[]
|
sources: string[]
|
||||||
|
isChecking: boolean
|
||||||
|
checkingCount: number
|
||||||
|
isDryRun?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user