mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-05 13:52:56 -04:00
[ENG-635] Job time estimation (#842)
* fix extension case sensitivity * job time estimation wip * update schema * use `DateTime` to handle estimated completion time * use a date with `dayjs` * remove old migrations * update migrations * remove dead code * Quick tweaks * unused import --------- Co-authored-by: brxken128 <77554505+brxken128@users.noreply.github.com> Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Camera,
|
||||
Copy,
|
||||
@@ -16,12 +17,11 @@ import { memo } from 'react';
|
||||
import { JobReport } from '@sd/client';
|
||||
import { ProgressBar } from '@sd/ui';
|
||||
import './Job.scss';
|
||||
import { useJobTimeText } from './useJobTimeText';
|
||||
|
||||
interface JobNiceData {
|
||||
name: string;
|
||||
icon: React.ForwardRefExoticComponent<any>;
|
||||
filesDiscovered: string;
|
||||
subtext: string;
|
||||
}
|
||||
|
||||
const getNiceData = (
|
||||
@@ -35,9 +35,7 @@ const getNiceData = (
|
||||
? `Indexed paths at ${job.metadata?.location_path} `
|
||||
: `Processing added location...`,
|
||||
icon: Folder,
|
||||
filesDiscovered: `${numberWithCommas(
|
||||
job.metadata?.total_paths || 0
|
||||
)} ${JobCountTextCondition(job, 'path')}`
|
||||
subtext: `${numberWithCommas(job.metadata?.total_paths || 0)} ${appendPlural(job, 'path')}`
|
||||
},
|
||||
thumbnailer: {
|
||||
name: `${
|
||||
@@ -46,12 +44,14 @@ const getNiceData = (
|
||||
: 'Generated thumbnails'
|
||||
}`,
|
||||
icon: Camera,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||
subtext: `${numberWithCommas(job.completed_task_count)} of ${numberWithCommas(
|
||||
job.task_count
|
||||
)} ${appendPlural(job, 'thumbnail')}`
|
||||
},
|
||||
shallow_thumbnailer: {
|
||||
name: `Generating thumbnails for current directory`,
|
||||
icon: Camera,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'item')}`
|
||||
},
|
||||
file_identifier: {
|
||||
name: `${
|
||||
@@ -60,60 +60,51 @@ const getNiceData = (
|
||||
: 'Extracted metadata'
|
||||
}`,
|
||||
icon: Eye,
|
||||
filesDiscovered:
|
||||
subtext:
|
||||
job.message ||
|
||||
`${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'item')}`
|
||||
`${numberWithCommas(job.metadata.total_orphan_paths)} ${appendPlural(
|
||||
job,
|
||||
'file',
|
||||
'file_identifier'
|
||||
)}`
|
||||
},
|
||||
object_validator: {
|
||||
name: `Generated full object hashes`,
|
||||
icon: Fingerprint,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(
|
||||
job,
|
||||
'object'
|
||||
)}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'object')}`
|
||||
},
|
||||
file_encryptor: {
|
||||
name: `Encrypted`,
|
||||
icon: LockSimple,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'file')}`
|
||||
},
|
||||
file_decryptor: {
|
||||
name: `Decrypted`,
|
||||
icon: LockSimpleOpen,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)}${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)}${appendPlural(job, 'file')}`
|
||||
},
|
||||
file_eraser: {
|
||||
name: `Securely erased`,
|
||||
icon: TrashSimple,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'file')}`
|
||||
},
|
||||
file_deleter: {
|
||||
name: `Deleted`,
|
||||
icon: Trash,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'file')}`
|
||||
},
|
||||
file_copier: {
|
||||
name: `Copied`,
|
||||
icon: Copy,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'file')}`
|
||||
},
|
||||
file_cutter: {
|
||||
name: `Moved`,
|
||||
icon: Scissors,
|
||||
filesDiscovered: `${numberWithCommas(job.task_count)} ${JobCountTextCondition(job, 'file')}`
|
||||
subtext: `${numberWithCommas(job.task_count)} ${appendPlural(job, 'file')}`
|
||||
}
|
||||
});
|
||||
|
||||
const StatusColors: Record<JobReport['status'], string> = {
|
||||
Running: 'text-blue-500',
|
||||
Failed: 'text-red-500',
|
||||
Completed: 'text-green-500',
|
||||
CompletedWithErrors: 'text-orange-500',
|
||||
Queued: 'text-yellow-500',
|
||||
Canceled: 'text-gray-500',
|
||||
Paused: 'text-gray-500'
|
||||
};
|
||||
|
||||
interface JobProps {
|
||||
job: JobReport;
|
||||
clearJob?: (arg: string) => void;
|
||||
@@ -121,15 +112,28 @@ interface JobProps {
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
function formatEstimatedRemainingTime(end_date: string) {
|
||||
const duration = dayjs.duration(new Date(end_date).getTime() - Date.now());
|
||||
|
||||
if (duration.hours() > 0) {
|
||||
return `${duration.hours()} hour${duration.hours() > 1 ? 's' : ''} remaining`;
|
||||
} else if (duration.minutes() > 0) {
|
||||
return `${duration.minutes()} minute${duration.minutes() > 1 ? 's' : ''} remaining`;
|
||||
} else {
|
||||
return `${duration.seconds()} second${duration.seconds() > 1 ? 's' : ''} remaining`;
|
||||
}
|
||||
}
|
||||
|
||||
function Job({ job, clearJob, className, isGroup }: JobProps) {
|
||||
const niceData = getNiceData(job, isGroup)[job.name] || {
|
||||
name: job.name,
|
||||
icon: Question,
|
||||
filesDiscovered: job.name
|
||||
subtext: job.name
|
||||
};
|
||||
const isRunning = job.status === 'Running';
|
||||
|
||||
const time = useJobTimeText(job);
|
||||
// dayjs from seconds to time
|
||||
const time = isRunning ? formatEstimatedRemainingTime(job.estimated_completion) : '';
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -148,20 +152,20 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="truncate">
|
||||
<span className="truncate font-semibold">{niceData.name}</span>
|
||||
<span className="font-semibold truncate">{niceData.name}</span>
|
||||
<p className="mb-[5px] mt-[2px] flex gap-1 truncate text-ink-faint">
|
||||
{job.status === 'Queued' && <p>{job.status}:</p>}
|
||||
{niceData.filesDiscovered}
|
||||
{niceData.subtext}
|
||||
{time && ' • '}
|
||||
<span className="truncate">{time}</span>
|
||||
</p>
|
||||
<div className="flex gap-1 truncate text-ink-faint"></div>
|
||||
</div>
|
||||
<div className="grow" />
|
||||
<div className="ml-7 flex flex-row space-x-2">
|
||||
<div className="flex flex-row space-x-2 ml-7">
|
||||
{/* {job.status === 'Running' && (
|
||||
<Button size="icon">
|
||||
<Tooltip label="Coming Soon">
|
||||
@@ -189,12 +193,18 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function JobCountTextCondition(job: JobReport, word: string) {
|
||||
const addStoEnd = job.task_count > 1 || job?.task_count === 0 ? `${word}s` : `${word}`;
|
||||
return addStoEnd;
|
||||
function appendPlural(job: JobReport, word: string, niceDataKey?: string) {
|
||||
const condition = (condition: boolean) => (condition ? `${word}s` : `${word}`);
|
||||
switch (niceDataKey) {
|
||||
case 'file_identifier':
|
||||
return condition(job.metadata?.total_orphan_paths > 1);
|
||||
default:
|
||||
return condition(job.task_count > 1);
|
||||
}
|
||||
}
|
||||
|
||||
function numberWithCommas(x: number) {
|
||||
if (!x) return 0;
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Folder } from '@sd/assets/icons';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { X } from 'phosphor-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { JobReport } from '@sd/client';
|
||||
import { Button, ProgressBar, Tooltip } from '@sd/ui';
|
||||
import Job from './Job';
|
||||
@@ -51,7 +51,7 @@ function JobGroup({ data, clearJob }: JobGroupProps) {
|
||||
size="icon"
|
||||
>
|
||||
<Tooltip label="Remove">
|
||||
<X className="h-4 w-4 cursor-pointer" />
|
||||
<X className="w-4 h-4 cursor-pointer" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)}
|
||||
@@ -67,17 +67,19 @@ function JobGroup({ data, clearJob }: JobGroupProps) {
|
||||
src={Folder}
|
||||
className={clsx('relative left-[-2px] top-2 z-10 mr-3 h-6 w-6')}
|
||||
/>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="truncate">
|
||||
<p className="truncate font-semibold">
|
||||
<p className="font-semibold truncate">
|
||||
{allJobsCompleted
|
||||
? `Added location "${data.metadata.init.location.name || ''}"`
|
||||
? `Added location "${
|
||||
data.metadata.init.location.name || ''
|
||||
}"`
|
||||
: `Indexing "${data.metadata.init.location.name || ''}"`}
|
||||
</p>
|
||||
<p className="my-[2px] text-ink-faint">
|
||||
<b>{tasks.total} </b>
|
||||
{tasks.total <= 1 ? 'item' : 'items'}
|
||||
{tasks.total <= 1 ? 'task' : 'tasks'}
|
||||
{' • '}
|
||||
{date_started}
|
||||
{!allJobsCompleted && totalGroupTime && ' • '}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { JobReport } from '@sd/client';
|
||||
import { useForceUpdate } from '~/util';
|
||||
|
||||
export function useJobTimeText(job: JobReport): string | null {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const elapsedTimeText = useMemo(() => {
|
||||
let newText: string;
|
||||
if (job.status === 'Running') {
|
||||
newText = `Elapsed ${dayjs(job.started_at).fromNow(true)}`;
|
||||
} else if (job.completed_at) {
|
||||
newText = `Took ${dayjs(job.started_at).from(job.completed_at, true)}`;
|
||||
} else {
|
||||
newText = `Took ${dayjs(job.started_at).fromNow(true)}`;
|
||||
}
|
||||
return newText;
|
||||
}, [job]);
|
||||
|
||||
useEffect(() => {
|
||||
if (job.status === 'Running') {
|
||||
const interval = setInterval(forceUpdate, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [job.status, forceUpdate]);
|
||||
|
||||
return elapsedTimeText === 'Took NaN years' ? null : elapsedTimeText;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
import { z } from '@sd/ui/src/forms';
|
||||
import { Category } from '~/../packages/client/src';
|
||||
import { useExplorerTopBarOptions } from '~/hooks';
|
||||
import Explorer from '../Explorer';
|
||||
import { SEARCH_PARAMS } from '../Explorer/util';
|
||||
@@ -11,8 +10,7 @@ import { TopBarPortal } from '../TopBar/Portal';
|
||||
import TopBarOptions from '../TopBar/TopBarOptions';
|
||||
import Statistics from '../overview/Statistics';
|
||||
import { Categories } from './Categories';
|
||||
import { useItems } from "./data"
|
||||
import { Category } from '~/../packages/client/src';
|
||||
import { useItems } from './data';
|
||||
|
||||
export type SearchArgs = z.infer<typeof SEARCH_PARAMS>;
|
||||
|
||||
@@ -46,8 +44,7 @@ export const Component = () => {
|
||||
isFetchingNextPage={query.isFetchingNextPage}
|
||||
scrollRef={page?.ref}
|
||||
>
|
||||
<Statistics />
|
||||
<Categories selected={selectedCategory} onSelectedChanged={setSelectedCategory}/>
|
||||
<Categories selected={selectedCategory} onSelectedChanged={setSelectedCategory} />
|
||||
</Explorer>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user