mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-22 15:40:07 -04:00
202 lines
5.6 KiB
TypeScript
202 lines
5.6 KiB
TypeScript
import clsx from 'clsx';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
ArrowsClockwise,
|
|
Camera,
|
|
Copy,
|
|
Eye,
|
|
Fingerprint,
|
|
Folder,
|
|
LockSimple,
|
|
LockSimpleOpen,
|
|
Pause,
|
|
Question,
|
|
Scissors,
|
|
Trash,
|
|
TrashSimple,
|
|
X
|
|
} from 'phosphor-react';
|
|
import { JobReport, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
|
import { Button, CategoryHeading, PopoverClose, ProgressBar, Tooltip } from '@sd/ui';
|
|
|
|
interface JobNiceData {
|
|
name: string;
|
|
icon: React.ForwardRefExoticComponent<any>;
|
|
}
|
|
|
|
const getNiceData = (job: JobReport): Record<string, JobNiceData> => ({
|
|
indexer: {
|
|
name: `Indexed ${numberWithCommas(job.metadata?.data?.total_paths || 0)} paths at "${
|
|
job.metadata?.data?.location_path || '?'
|
|
}"`,
|
|
icon: Folder
|
|
},
|
|
thumbnailer: {
|
|
name: `Generated ${numberWithCommas(job.task_count)} thumbnails`,
|
|
icon: Camera
|
|
},
|
|
file_identifier: {
|
|
name: `Extracted metadata for ${numberWithCommas(job.metadata?.total_orphan_paths || 0)} files`,
|
|
icon: Eye
|
|
},
|
|
object_validator: {
|
|
name: `Generated ${numberWithCommas(job.task_count)} full object hashes`,
|
|
icon: Fingerprint
|
|
},
|
|
file_encryptor: {
|
|
name: `Encrypted ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: LockSimple
|
|
},
|
|
file_decryptor: {
|
|
name: `Decrypted ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: LockSimpleOpen
|
|
},
|
|
file_eraser: {
|
|
name: `Securely erased ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: TrashSimple
|
|
},
|
|
file_deleter: {
|
|
name: `Deleted ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: Trash
|
|
},
|
|
file_copier: {
|
|
name: `Copied ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: Copy
|
|
},
|
|
file_cutter: {
|
|
name: `Moved ${numberWithCommas(job.task_count)} ${
|
|
job.task_count > 1 || job.task_count === 0 ? 'files' : 'file'
|
|
}`,
|
|
icon: Scissors
|
|
}
|
|
});
|
|
|
|
const StatusColors: Record<JobReport['status'], string> = {
|
|
Running: 'text-blue-500',
|
|
Failed: 'text-red-500',
|
|
Completed: 'text-green-500',
|
|
Queued: 'text-yellow-500',
|
|
Canceled: 'text-gray-500',
|
|
Paused: 'text-gray-500'
|
|
};
|
|
|
|
export function JobsManager() {
|
|
const runningJobs = useLibraryQuery(['jobs.getRunning']);
|
|
const jobs = useLibraryQuery(['jobs.getHistory']);
|
|
const clearAllJobs = useLibraryMutation(['jobs.clearAll']);
|
|
|
|
return (
|
|
<div className="h-full overflow-hidden pb-10">
|
|
<div className="z-20 flex h-10 w-full items-center rounded-t-md border-b border-app-line/50 bg-app-button/70 px-2">
|
|
<CategoryHeading className="ml-2">Recent Jobs</CategoryHeading>
|
|
<div className="grow" />
|
|
|
|
<Button onClick={() => clearAllJobs.mutate(null)} size="icon">
|
|
<Tooltip label="Clear out finished jobs">
|
|
<Trash className="h-5 w-5" />
|
|
</Tooltip>
|
|
</Button>
|
|
<PopoverClose asChild>
|
|
<Button size="icon">
|
|
<Tooltip label="Close">
|
|
<X className="h-5 w-5" />
|
|
</Tooltip>
|
|
</Button>
|
|
</PopoverClose>
|
|
</div>
|
|
<div className="custom-scroll inspector-scroll mr-1 h-full overflow-x-hidden">
|
|
<div className="">
|
|
<div className="py-1">
|
|
{runningJobs.data?.map((job) => (
|
|
<Job key={job.id} job={job} />
|
|
))}
|
|
{jobs.data?.map((job) => (
|
|
<Job key={job.id} job={job} />
|
|
))}
|
|
{jobs.data?.length === 0 && runningJobs.data?.length === 0 && (
|
|
<div className="flex h-32 items-center justify-center text-ink-dull">No jobs.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Job({ job }: { job: JobReport }) {
|
|
const niceData = getNiceData(job)[job.name] || {
|
|
name: job.name,
|
|
icon: Question
|
|
};
|
|
const isRunning = job.status === 'Running';
|
|
return (
|
|
// Do we actually need bg-opacity-60 here? Where is the bg?
|
|
<div className="flex items-center border-b border-app-line/50 bg-opacity-60 p-2 pl-4">
|
|
<Tooltip label={job.status}>
|
|
<niceData.icon className={clsx('mr-3 h-5 w-5')} />
|
|
</Tooltip>
|
|
<div className="flex flex-col truncate">
|
|
<span className="mt-0.5 truncate font-semibold">
|
|
{isRunning ? job.message : niceData.name}
|
|
</span>
|
|
{isRunning && (
|
|
<div className="my-1 w-full">
|
|
<ProgressBar value={job.completed_task_count} total={job.task_count} />
|
|
</div>
|
|
)}
|
|
<div className="flex items-center truncate text-ink-faint">
|
|
<span className="text-xs">
|
|
{isRunning ? 'Elapsed' : job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
|
|
{job.seconds_elapsed
|
|
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
|
|
: 'less than a second'}
|
|
</span>
|
|
<span className="mx-1 opacity-50">•</span>
|
|
{
|
|
<span className="text-xs">
|
|
{isRunning ? 'Unknown time remaining' : dayjs(job.date_created).toNow(true) + ' ago'}
|
|
</span>
|
|
}
|
|
</div>
|
|
{/* <span className="mt-0.5 opacity-50 text-tiny text-ink-faint">{job.id}</span> */}
|
|
</div>
|
|
<div className="grow" />
|
|
<div className="ml-7 flex flex-row space-x-2">
|
|
{job.status === 'Running' && (
|
|
<Button size="icon">
|
|
<Tooltip label="Pause">
|
|
<Pause className="h-4 w-4" />
|
|
</Tooltip>
|
|
</Button>
|
|
)}
|
|
{job.status === 'Failed' && (
|
|
<Button size="icon">
|
|
<Tooltip label="Retry">
|
|
<ArrowsClockwise className="w-4" />
|
|
</Tooltip>
|
|
</Button>
|
|
)}
|
|
<Button size="icon">
|
|
<Tooltip label="Remove">
|
|
<X className="h-4 w-4" />
|
|
</Tooltip>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function numberWithCommas(x: number) {
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|