Files
spacedrive/interface/app/$libraryId/Layout/Sidebar/JobManager/JobContainer.tsx
Consoli 9e09d92f03 [ENG-1159] Report progress when copying files (#2538)
* Copy ephemeral files by chunks

* Improving buffered writer

* Report progress

* Copy and emit progress as two separated tasks

* Simplify for-if-return with `Iterator::any`

* Docs and use structured tracing

* Simplify code using paths

* wip

* wip

* wip

* wip

* Add report of messages while copying files

* Add info field to job report

* Group paths from OldFileCopierJobStep into a single struct

* Improve progress information

* Remove the need for synchronization

* Error handling for copy

* Clean up frontend

* Make watcher only consider new files

* Fix concurrent renaming of multiple files that have the same name

* Add documentation for file strategist

* Remove editions to ephemeral file copies

* Remove experimental/nightly features from Cargo.toml

* Fix no-case-declarations

* Remove dead comments

* Format code

* Use "Duplicate" instead of "Copy" in the frontend messages

* Remove inline always to make clippy happy

* icons for deleter and copier

* Fix JobManager for copy jobs
 - Fix some types definitions

* Fix Job icon choosing logic
 - Fix Copier job showing two progress bars

* Log which files are not formatted in the Type and style check CI

* fmt

* Forgot an import

* autoformat

---------

Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
2024-08-17 02:00:59 +00:00

168 lines
4.4 KiB
TypeScript

import clsx from 'clsx';
import {
forwardRef,
ForwardRefExoticComponent,
Fragment,
HTMLAttributes,
ReactNode,
useEffect,
useState
} from 'react';
import { TextItems } from '@sd/client';
import { Tooltip, tw } from '@sd/ui';
import classes from './Job.module.scss';
interface JobContainerProps extends HTMLAttributes<HTMLLIElement> {
name: string;
icon?: string | ForwardRefExoticComponent<any>;
textItems?: TextItems;
isChild?: boolean;
children?: ReactNode;
eta?: number;
status?: string;
}
const CIRCLE_ICON_CLASS =
'relative flex-shrink-0 top-1 z-20 mr-3 h-7 w-7 rounded-full bg-app-button p-[5.5px]';
const IMG_ICON_CLASS = 'relative left-[-2px] top-1 z-10 mr-2 h-8 w-8';
const MetaContainer = tw.div`flex w-full overflow-hidden flex-col`;
const TextLine = tw.div`mt-[2px] gap-1 text-ink-faint truncate mr-8 pl-1.5`;
const TextItem = tw.span`truncate`;
const formatETA = (eta: number): string => {
const seconds = Math.floor((eta / 1000) % 60);
const minutes = Math.floor((eta / (1000 * 60)) % 60);
const hours = Math.floor((eta / (1000 * 60 * 60)) % 24);
const days = Math.floor(eta / (1000 * 60 * 60 * 24));
let formattedETA = '';
if (days > 0) formattedETA += `${days} day${days > 1 ? 's' : ''} `;
if (hours > 0) formattedETA += `${hours} hour${hours > 1 ? 's' : ''} `;
if (minutes > 0) formattedETA += `${minutes} minute${minutes > 1 ? 's' : ''} `;
if (seconds > 0 || formattedETA === '')
formattedETA += `${seconds} second${seconds != 1 ? 's' : ''} `;
return formattedETA.trim() + ' remaining';
};
// Job container consolidates the common layout of a job item, used for regular jobs (Job.tsx) and grouped jobs (JobGroup.tsx).
const JobContainer = forwardRef<HTMLLIElement, JobContainerProps>((props, ref) => {
const {
name,
icon: Icon,
textItems,
isChild,
children,
className,
eta,
status,
...restProps
} = props;
const [currentETA, setCurrentETA] = useState<number | undefined>(eta);
useEffect(() => {
if (currentETA != null && currentETA > 0) {
const interval = setInterval(() => {
setCurrentETA((prevETA) => {
if (prevETA === undefined || prevETA <= 1000) return 0;
return prevETA - 1000;
});
}, 1000);
return () => clearInterval(interval);
}
}, [currentETA]);
useEffect(() => {
setCurrentETA(eta);
}, [eta]);
return (
<li
ref={ref}
className={clsx(
'relative flex border-b border-app-line/50 px-4 py-3',
isChild && classes.jobGroupChild,
isChild && 'border-none bg-app-darkBox p-2 pl-10',
className
)}
{...restProps}
>
{typeof Icon === 'string' ? (
<img src={Icon} className={IMG_ICON_CLASS} />
) : (
Icon && (
<Icon weight="fill" className={clsx(CIRCLE_ICON_CLASS, isChild && 'mx-1')} />
)
)}
<MetaContainer>
<Tooltip
labelClassName="break-all"
asChild
tooltipClassName="max-w-[400px]"
position="top"
label={name}
>
<p className="w-fit max-w-[83%] truncate pl-1.5 font-semibold">{name}</p>
</Tooltip>
{textItems?.map((item, index) => {
const filteredItems = item.filter((i) => i?.text);
const popoverText = filteredItems.map((i) => i?.text).join(' • ');
return (
<Tooltip
label={popoverText}
key={index}
tooltipClassName="max-w-[400px] tabular-nums"
>
<TextLine>
{filteredItems.map((textItem, index) => {
const Icon = textItem?.icon;
return (
<Fragment key={index}>
<TextItem
onClick={textItem?.onClick}
className={clsx(
'tabular-nums',
textItem?.onClick &&
'-ml-1.5 rounded-md hover:bg-app-button/50'
)}
>
{Icon && (
<Icon
weight="fill"
className="-mt-0.5 ml-[5px] mr-1 inline"
/>
)}
{textItem?.text}
</TextItem>
{index < filteredItems.length - 1 && (
<span className="truncate"> </span>
)}
</Fragment>
);
})}
{status == 'Running' && (
<div className="text-[0.8rem] text-gray-400 opacity-60">
{currentETA !== undefined
? formatETA(currentETA)
: 'Unable to calculate estimated completion time'}
</div>
)}
</TextLine>
</Tooltip>
);
})}
<div className="mt-1">{children}</div>
</MetaContainer>
</li>
);
});
export default JobContainer;