[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:
Jamie Pine
2023-05-23 15:21:52 -07:00
committed by GitHub
parent da2e78dc61
commit ea46e7736a
9 changed files with 157 additions and 106 deletions

View File

@@ -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, ',');
}

View File

@@ -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 && ' • '}

View File

@@ -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;
}

View File

@@ -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>
</>
);