mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-22 23:48:26 -04:00
* 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>
168 lines
4.4 KiB
TypeScript
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;
|