diff --git a/.changeset/ninety-toes-cheer.md b/.changeset/ninety-toes-cheer.md new file mode 100644 index 000000000..b8b862069 --- /dev/null +++ b/.changeset/ninety-toes-cheer.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +Transfers now list remote uploads originating from other devices. diff --git a/.changeset/short-ducks-press.md b/.changeset/short-ducks-press.md new file mode 100644 index 000000000..04875dcde --- /dev/null +++ b/.changeset/short-ducks-press.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +Remote file uploads can now be aborted from the transfer list. diff --git a/apps/renterd/components/TransfersBar.tsx b/apps/renterd/components/TransfersBar.tsx index f5fb0048b..3cde2ff22 100644 --- a/apps/renterd/components/TransfersBar.tsx +++ b/apps/renterd/components/TransfersBar.tsx @@ -5,9 +5,11 @@ import { ProgressBar, ScrollArea, Text, + Tooltip, } from '@siafoundation/design-system' import { Close16, + CloudUpload16, Download16, Subtract24, Upload16, @@ -15,6 +17,7 @@ import { import { useState } from 'react' import { useFilesManager } from '../contexts/filesManager' import { useAppSettings } from '@siafoundation/react-core' +import { upperFirst } from '@technically/lodash' function getProgress(transfer: { loaded?: number; size?: number }) { return transfer.loaded !== undefined ? transfer.loaded / transfer.size : 1 @@ -22,18 +25,24 @@ function getProgress(transfer: { loaded?: number; size?: number }) { export function TransfersBar() { const { isUnlockedAndAuthedRoute } = useAppSettings() - const { uploadsList, uploadCancel, downloadsList, downloadCancel } = - useFilesManager() + const { + uploadsList, + remoteUploadsList, + uploadCancel, + downloadsList, + downloadCancel, + } = useFilesManager() const [maximized, setMaximized] = useState(true) const uploadCount = uploadsList.length + const remoteUploadCount = remoteUploadsList.length const downloadCount = downloadsList.length if (!isUnlockedAndAuthedRoute) { return null } - if (uploadCount === 0 && downloadCount === 0) { + if (uploadCount === 0 && downloadCount === 0 && remoteUploadCount === 0) { return null } @@ -91,6 +100,45 @@ export function TransfersBar() { })} ) : null} + {remoteUploadCount > 0 ? ( + <> +
+ + Remote uploads ({remoteUploadCount}) + + +
+ {remoteUploadsList.map((upload) => { + return ( +
+
+ + {upload.path} + + +
+
+ + Uploading from a different device + +
+
+ ) + })} + + ) : null} {downloadCount > 0 ? ( <>
@@ -152,16 +200,28 @@ export function TransfersBar() {
diff --git a/apps/renterd/contexts/filesManager/index.tsx b/apps/renterd/contexts/filesManager/index.tsx index d821cf6a4..831b09eb3 100644 --- a/apps/renterd/contexts/filesManager/index.tsx +++ b/apps/renterd/contexts/filesManager/index.tsx @@ -23,6 +23,7 @@ import { useDownloads } from './downloads' import { useBuckets } from '@siafoundation/react-renterd' import { routes } from '../../config/routes' import useLocalStorageState from 'use-local-storage-state' +import { useRemoteUploads } from './remoteUploads' function useFilesManagerMain() { const { @@ -87,9 +88,13 @@ function useFilesManagerMain() { [router, activeDirectory] ) - const { uploadFiles, uploadsList, uploadCancel } = useUploads({ + const { uploadFiles, uploadsMap, uploadsList, uploadCancel } = useUploads({ activeDirectoryPath, }) + const { remoteUploadsList } = useRemoteUploads({ + activeBucket, + uploadsMap, + }) const { downloadFiles, downloadsList, getFileUrl, downloadCancel } = useDownloads() @@ -174,6 +179,7 @@ function useFilesManagerMain() { navigateToModeSpecificFiltering, uploadFiles, uploadsList, + remoteUploadsList, uploadCancel, downloadFiles, downloadsList, diff --git a/apps/renterd/contexts/filesManager/remoteUploads.tsx b/apps/renterd/contexts/filesManager/remoteUploads.tsx new file mode 100644 index 000000000..4803ba8dc --- /dev/null +++ b/apps/renterd/contexts/filesManager/remoteUploads.tsx @@ -0,0 +1,66 @@ +import { + Bucket, + useMultipartUploadAbort, + useMultipartUploadListUploads, +} from '@siafoundation/react-renterd' +import { useMemo } from 'react' +import { ObjectUploadData, ObjectUploadRemoteData, UploadsMap } from './types' +import { getFilename, join } from '../../lib/paths' + +type Props = { + activeBucket?: Bucket + uploadsMap: UploadsMap +} + +export function useRemoteUploads({ activeBucket, uploadsMap }: Props) { + const apiBusUploadAbort = useMultipartUploadAbort() + const remoteUploads = useMultipartUploadListUploads({ + disabled: !activeBucket, + payload: { + bucket: activeBucket?.name, + }, + }) + + const remoteUploadsMap = useMemo(() => { + return ( + remoteUploads.data?.uploads.reduce((acc, upload) => { + const id = upload.uploadID + const name = getFilename(upload.path) + const fullPath = join(activeBucket?.name, upload.path) + return { + ...acc, + [id]: { + id, + path: fullPath, + bucket: activeBucket, + name, + size: 1, + loaded: 1, + isUploading: true, + remote: true, + type: 'file', + uploadAbort: () => + apiBusUploadAbort.post({ + payload: { + bucket: activeBucket?.name, + path: upload.path, + uploadID: upload.uploadID, + }, + }), + } as ObjectUploadRemoteData, + } + }, {}) || {} + ) + }, [activeBucket, remoteUploads.data, apiBusUploadAbort]) + + const remoteUploadsList = useMemo(() => { + // remoteUploadsMap without anything in uploadsMap + return Object.values(remoteUploadsMap).filter( + (remoteUpload: ObjectUploadData) => !uploadsMap[remoteUpload.id] + ) as ObjectUploadData[] + }, [remoteUploadsMap, uploadsMap]) + + return { + remoteUploadsList, + } +}