From 713e839e898332c9dcaa2e7a6dbebda33b41d04b Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Mon, 6 Jan 2025 15:50:59 -0500 Subject: [PATCH] feat(renterd): uploads list local and remote --- .changeset/sweet-onions-stare.md | 5 + apps/renterd/components/TransfersBar.tsx | 8 +- .../Uploads/EmptyState/StateNoneMatching.tsx | 29 --- .../Uploads/EmptyState/StateNoneYet.tsx | 29 --- .../components/Uploads/EmptyState/index.tsx | 27 -- .../Uploads/{EmptyState => }/StateError.tsx | 0 .../components/Uploads/StateNoneYet.tsx | 47 ++++ .../components/Uploads/UploadsStatsMenu.tsx | 60 +++++ .../Uploads/UploadsStatsMenu/index.tsx | 30 --- .../components/Uploads/UploadsTable.tsx | 31 ++- .../Uploads/UploadsViewDropdownMenu.tsx | 10 +- apps/renterd/contexts/uploads/index.tsx | 234 +++--------------- .../contexts/uploads/useLocalUploads.tsx | 49 ++++ .../contexts/uploads/useRemoteUploads.tsx | 152 ++++++++++++ .../src/hooks/useResetPagination.ts | 8 - libs/design-system/src/index.ts | 1 + 16 files changed, 382 insertions(+), 338 deletions(-) create mode 100644 .changeset/sweet-onions-stare.md delete mode 100644 apps/renterd/components/Uploads/EmptyState/StateNoneMatching.tsx delete mode 100644 apps/renterd/components/Uploads/EmptyState/StateNoneYet.tsx delete mode 100644 apps/renterd/components/Uploads/EmptyState/index.tsx rename apps/renterd/components/Uploads/{EmptyState => }/StateError.tsx (100%) create mode 100644 apps/renterd/components/Uploads/StateNoneYet.tsx create mode 100644 apps/renterd/components/Uploads/UploadsStatsMenu.tsx delete mode 100644 apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx create mode 100644 apps/renterd/contexts/uploads/useLocalUploads.tsx create mode 100644 apps/renterd/contexts/uploads/useRemoteUploads.tsx diff --git a/.changeset/sweet-onions-stare.md b/.changeset/sweet-onions-stare.md new file mode 100644 index 000000000..5e9219fdb --- /dev/null +++ b/.changeset/sweet-onions-stare.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The uploads list now has two views, one for local uploads only and one for all uploads including from other devices. diff --git a/apps/renterd/components/TransfersBar.tsx b/apps/renterd/components/TransfersBar.tsx index 69acc0eb5..a4c0a2f7d 100644 --- a/apps/renterd/components/TransfersBar.tsx +++ b/apps/renterd/components/TransfersBar.tsx @@ -16,10 +16,12 @@ export function TransfersBar() { const { isUnlockedAndAuthedRoute } = useAppSettings() const { downloadsList, downloadCancel, isViewingUploads, navigateToUploads } = useFilesManager() - const { datasetPageTotal: uploadsPageTotal } = useUploads() + const { + localUploads: { datasetTotal: uploadsTotal }, + } = useUploads() const [maximized, setMaximized] = useState(true) - const isActiveUploads = !!uploadsPageTotal + const isActiveUploads = !!uploadsTotal const downloadCount = downloadsList.length const isActiveDownloads = !!downloadCount @@ -40,7 +42,7 @@ export function TransfersBar() { className="flex gap-1" > - Active uploads + Active uploads ({uploadsTotal.toLocaleString()}) ) : null} {isActiveDownloads ? ( diff --git a/apps/renterd/components/Uploads/EmptyState/StateNoneMatching.tsx b/apps/renterd/components/Uploads/EmptyState/StateNoneMatching.tsx deleted file mode 100644 index 8f33f359b..000000000 --- a/apps/renterd/components/Uploads/EmptyState/StateNoneMatching.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, Text } from '@siafoundation/design-system' -import { Filter32 } from '@siafoundation/react-icons' -import { useFilesManager } from '../../../contexts/filesManager' - -export function StateNoneMatching() { - const { filters, resetFilters } = useFilesManager() - return ( -
- - - -
- - No uploads matching filters. - - {!!filters.length && ( - - )} -
-
- ) -} diff --git a/apps/renterd/components/Uploads/EmptyState/StateNoneYet.tsx b/apps/renterd/components/Uploads/EmptyState/StateNoneYet.tsx deleted file mode 100644 index 4bdc3b43c..000000000 --- a/apps/renterd/components/Uploads/EmptyState/StateNoneYet.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Code, LinkButton, Text } from '@siafoundation/design-system' -import { CloudUpload32 } from '@siafoundation/react-icons' -import { routes } from '../../../config/routes' -import { useFilesManager } from '../../../contexts/filesManager' - -export function StateNoneYet() { - const { activeBucketName: activeBucket } = useFilesManager() - return ( -
- - - -
- - The {activeBucket} bucket does not have any active - uploads. - - { - e.stopPropagation() - }} - > - View buckets list - -
-
- ) -} diff --git a/apps/renterd/components/Uploads/EmptyState/index.tsx b/apps/renterd/components/Uploads/EmptyState/index.tsx deleted file mode 100644 index 58047f6f1..000000000 --- a/apps/renterd/components/Uploads/EmptyState/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { StateError } from './StateError' -import { StateNoneMatching } from './StateNoneMatching' -import { StateNoneYet } from './StateNoneYet' -import { useUploads } from '../../../contexts/uploads' -import { StateNoneOnPage } from '@siafoundation/design-system' - -export function EmptyState() { - const { datasetState } = useUploads() - - if (datasetState === 'noneOnPage') { - return - } - - if (datasetState === 'noneMatchingFilters') { - return - } - - if (datasetState === 'error') { - return - } - - if (datasetState === 'noneYet') { - return - } - - return null -} diff --git a/apps/renterd/components/Uploads/EmptyState/StateError.tsx b/apps/renterd/components/Uploads/StateError.tsx similarity index 100% rename from apps/renterd/components/Uploads/EmptyState/StateError.tsx rename to apps/renterd/components/Uploads/StateError.tsx diff --git a/apps/renterd/components/Uploads/StateNoneYet.tsx b/apps/renterd/components/Uploads/StateNoneYet.tsx new file mode 100644 index 000000000..3459c3550 --- /dev/null +++ b/apps/renterd/components/Uploads/StateNoneYet.tsx @@ -0,0 +1,47 @@ +import { Code, LinkButton, Text } from '@siafoundation/design-system' +import { CloudUpload32 } from '@siafoundation/react-icons' +import { routes } from '../../config/routes' +import { useFilesManager } from '../../contexts/filesManager' +import { useUploads } from '../../contexts/uploads' + +export function StateNoneYet() { + const { activeView } = useUploads() + const { activeBucketName: activeBucket } = useFilesManager() + + const href = activeBucket + ? routes.buckets.files + .replace('[bucket]', activeBucket) + .replace('[path]', '') + : routes.buckets.index + + return ( +
+ + + +
+ + {activeView === 'localUploads' ? ( + <> + The {activeBucket} bucket does not have any active + uploads from this session. + + ) : ( + <> + The {activeBucket} bucket does not have any active + uploads. + + )} + + { + e.stopPropagation() + }} + > + {activeBucket ? 'View files' : 'View buckets list'} + +
+
+ ) +} diff --git a/apps/renterd/components/Uploads/UploadsStatsMenu.tsx b/apps/renterd/components/Uploads/UploadsStatsMenu.tsx new file mode 100644 index 000000000..88354be3e --- /dev/null +++ b/apps/renterd/components/Uploads/UploadsStatsMenu.tsx @@ -0,0 +1,60 @@ +import { + Button, + PaginatorKnownTotal, + PaginatorMarker, +} from '@siafoundation/design-system' +import { useUploads } from '../../contexts/uploads' + +export function UploadsStatsMenu() { + const { + activeData, + abortAll, + activeView, + remoteUploads, + localUploads, + setActiveView, + } = useUploads() + + const paginatorEl = + activeView === 'globalUploads' ? ( + + ) : ( + + ) + + return ( +
+ + +
+ {activeData.datasetPageTotal > 0 && ( + + )} + {paginatorEl} +
+ ) +} diff --git a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx b/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx deleted file mode 100644 index 9f7b8616b..000000000 --- a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Button, PaginatorMarker } from '@siafoundation/design-system' -import { useUploads } from '../../../contexts/uploads' - -export function UploadsStatsMenu() { - const { - abortAll, - limit, - datasetPageTotal, - datasetState, - marker, - nextMarker, - hasMore, - } = useUploads() - return ( -
-
- {datasetPageTotal > 0 && ( - - )} - -
- ) -} diff --git a/apps/renterd/components/Uploads/UploadsTable.tsx b/apps/renterd/components/Uploads/UploadsTable.tsx index f1b9aff3a..784eab2be 100644 --- a/apps/renterd/components/Uploads/UploadsTable.tsx +++ b/apps/renterd/components/Uploads/UploadsTable.tsx @@ -1,23 +1,32 @@ -import { Table } from '@siafoundation/design-system' -import { EmptyState } from './EmptyState' +import { EmptyState, Table } from '@siafoundation/design-system' import { useUploads } from '../../contexts/uploads' +import { StateNoneYet } from './StateNoneYet' +import { StateError } from './StateError' export function UploadsTable() { const { - visibleColumns, - sortableColumns, - toggleSort, - datasetPage, - datasetState, - sortField, - sortDirection, + activeData: { datasetPage }, + activeTableState: { + visibleColumns, + sortableColumns, + toggleSort, + sortField, + sortDirection, + }, + activeDatasetState, } = useUploads() return (
} + isLoading={activeDatasetState === 'loading'} + emptyState={ + } + error={} + /> + } pageSize={10} data={datasetPage} columns={visibleColumns} diff --git a/apps/renterd/components/Uploads/UploadsViewDropdownMenu.tsx b/apps/renterd/components/Uploads/UploadsViewDropdownMenu.tsx index a39b51e97..a64fa5936 100644 --- a/apps/renterd/components/Uploads/UploadsViewDropdownMenu.tsx +++ b/apps/renterd/components/Uploads/UploadsViewDropdownMenu.tsx @@ -11,10 +11,12 @@ import { useUploads } from '../../contexts/uploads' export function UploadsViewDropdownMenu() { const { - configurableColumns, - toggleColumnVisibility, - resetDefaultColumnVisibility, - visibleColumnIds, + activeTableState: { + configurableColumns, + toggleColumnVisibility, + resetDefaultColumnVisibility, + visibleColumnIds, + }, } = useUploads() return ( >(() => { - if (!activeBucket?.name) { - return undefined - } - return { - bucket: activeBucket?.name, - uploadIDMarker: markers?.uploadIDMarker || undefined, - keyMarker: markers?.keyMarker || undefined, - limit, - } - }, [activeBucket, limit, markers]) - - const response = useMultipartUploadListUploads({ - disabled: !payload, - payload: payload as MultipartUploadListUploadsPayload, - config: { - swr: { - keepPreviousData: true, - }, + const { activeBucket } = useFilesManager() + const [activeView, _setActiveView] = useState< + 'localUploads' | 'globalUploads' + >('localUploads') + const resetPagination = useResetPagination() + const setActiveView = useCallback( + (view: 'localUploads' | 'globalUploads') => { + resetPagination() + _setActiveView(view) }, - }) + [resetPagination] + ) + const remoteUploads = useRemoteUploads() + const localUploads = useLocalUploads() + const activePage = + activeView === 'localUploads' + ? localUploads.datasetPage + : remoteUploads.datasetPage const abortAll = useCallback(async () => { - if (!response.data?.uploads || !activeBucket?.name) { + if (!activePage || !activeBucket?.name) { return } - return Promise.all( - response.data.uploads.map(async (upload) => { - const localUpload = uploadsMap[upload.uploadID] - if (localUpload) { - localUpload.uploadAbort?.() - } else { - await apiBusUploadAbort.post({ - payload: { - bucket: activeBucket.name, - key: upload.key, - uploadID: upload.uploadID, - }, - }) - } - }) - ) - }, [response.data, apiBusUploadAbort, activeBucket, uploadsMap]) - - const datasetPage = useMemo>(() => { - const uploads = maybeFromNullishArrayResponse(response.data?.uploads) - if (!uploads || !activeBucket?.name) { - return undefined - } - return uploads.map((upload) => { - const id = upload.uploadID - const key = upload.key - const name = getFilename(key) - const fullPath = join(activeBucket.name, upload.key) - const localUpload = uploadsMap[getUploadId(fullPath)] - if (localUpload) { - return localUpload - } - return { - id, - path: fullPath, - key, - bucket: activeBucket, - name, - size: 1, - loaded: 1, - isUploading: true, - uploadStatus: 'uploading', - createdAt: upload.createdAt, - remote: true, - type: 'file', - uploadAbort: async () => { - await apiBusUploadAbort.post({ - payload: { - bucket: activeBucket?.name, - key: upload.key, - uploadID: upload.uploadID, - }, - }) - }, - } - }) - }, [uploadsMap, activeBucket, response, apiBusUploadAbort]) - - const { - configurableColumns, - visibleColumnIds, - visibleColumns, - sortableColumns, - toggleColumnVisibility, - setColumnsVisible, - setColumnsHidden, - toggleSort, - setSortDirection, - setSortField, - sortField, - sortDirection, - resetDefaultColumnVisibility, - } = useTableState('renterd/v0/uploads', { - columns, - columnsDefaultVisible, - sortOptions, - defaultSortField, - }) - - const datasetState = useDatasetState({ - datasetPage, - isValidating: response.isValidating, - error: response.error, - marker, - filters, - }) - - const nextMarker = useBuildMarkerParam({ - uploadIDMarker: response.data?.nextUploadIDMarker || '', - keyMarker: response.data?.nextMarker || '', - }) + return Promise.all(activePage.map((upload) => upload.uploadAbort?.())) + }, [activePage, activeBucket]) return { + activeView, + setActiveView, + activeData: activeView === 'localUploads' ? localUploads : remoteUploads, + remoteUploads, + localUploads, + activeDatasetState: + activeView === 'localUploads' + ? localUploads.datasetState + : remoteUploads.datasetState, + activeTableState: + activeView === 'localUploads' + ? localUploads.tableState + : remoteUploads.tableState, abortAll, - datasetState, - limit, - marker, - nextMarker, - hasMore: !!response.data?.hasMore, - isLoading: response.isLoading, - error: response.error, - datasetPageTotal: datasetPage?.length || 0, - visibleColumns, - datasetPage, - configurableColumns, - visibleColumnIds, - sortableColumns, - toggleColumnVisibility, - setColumnsVisible, - setColumnsHidden, - toggleSort, - setSortDirection, - setSortField, - sortField, - filters, - setFilter, - removeFilter, - removeLastFilter, - resetFilters, - sortDirection, - resetDefaultColumnVisibility, } } @@ -196,31 +64,3 @@ export function UploadsProvider({ children }: Props) { {children} ) } - -function useMarkersFromParam(marker: Nullable) { - return useMemo>(() => { - if (marker) { - const [uploadIDMarker, keyMarker] = marker.split('') - if (uploadIDMarker && keyMarker) { - return { keyMarker, uploadIDMarker } - } - return undefined - } - return undefined - }, [marker]) -} - -function useBuildMarkerParam({ - uploadIDMarker, - keyMarker, -}: { - uploadIDMarker: string - keyMarker: string -}): Nullable { - return useMemo(() => { - if (!uploadIDMarker || !keyMarker) { - return null - } - return `${uploadIDMarker}${keyMarker}` - }, [uploadIDMarker, keyMarker]) -} diff --git a/apps/renterd/contexts/uploads/useLocalUploads.tsx b/apps/renterd/contexts/uploads/useLocalUploads.tsx new file mode 100644 index 000000000..e01fce29a --- /dev/null +++ b/apps/renterd/contexts/uploads/useLocalUploads.tsx @@ -0,0 +1,49 @@ +import { + useDatasetState, + usePaginationOffset, + useTableState, +} from '@siafoundation/design-system' +import { useMemo } from 'react' +import { columnsDefaultVisible, defaultSortField, sortOptions } from './types' +import { columns } from './columns' +import { useFilesManager } from '../filesManager' +import { ObjectUploadData } from '../filesManager/types' +import { Maybe } from '@siafoundation/types' + +const defaultLimit = 500 + +export function useLocalUploads() { + const { uploadsList } = useFilesManager() + const { limit, offset } = usePaginationOffset(defaultLimit) + + const datasetPage = useMemo>(() => { + return uploadsList.slice(offset, offset + limit) + }, [uploadsList, offset, limit]) + + const datasetState = useDatasetState({ + datasetPage, + isValidating: false, + offset, + error: undefined, + }) + + const tableState = useTableState('renterd/v0/uploads', { + columns, + columnsDefaultVisible, + sortOptions, + defaultSortField, + }) + + return { + datasetState, + limit, + offset, + hasMore: uploadsList.length > offset + limit, + isLoading: false, + error: undefined, + datasetTotal: uploadsList.length, + datasetPageTotal: datasetPage?.length || 0, + datasetPage, + tableState, + } +} diff --git a/apps/renterd/contexts/uploads/useRemoteUploads.tsx b/apps/renterd/contexts/uploads/useRemoteUploads.tsx new file mode 100644 index 000000000..c1d09d804 --- /dev/null +++ b/apps/renterd/contexts/uploads/useRemoteUploads.tsx @@ -0,0 +1,152 @@ +import { + useDatasetState, + usePaginationMarker, + useTableState, +} from '@siafoundation/design-system' +import { + useMultipartUploadAbort, + useMultipartUploadListUploads, +} from '@siafoundation/renterd-react' +import { useMemo } from 'react' +import { columnsDefaultVisible, defaultSortField, sortOptions } from './types' +import { columns } from './columns' +import { join, getFilename } from '../../lib/paths' +import { useFilesManager } from '../filesManager' +import { ObjectUploadData } from '../filesManager/types' +import { MultipartUploadListUploadsPayload } from '@siafoundation/renterd-types' +import { maybeFromNullishArrayResponse } from '@siafoundation/react-core' +import { Maybe, Nullable } from '@siafoundation/types' +import { getUploadId } from '../filesManager/uploads' + +const defaultLimit = 500 + +export function useRemoteUploads() { + const { uploadsMap, activeBucket } = useFilesManager() + const { limit, marker } = usePaginationMarker(defaultLimit) + const markers = useMarkersFromParam(marker) + + const apiBusUploadAbort = useMultipartUploadAbort() + + const payload = useMemo>(() => { + if (!activeBucket?.name) { + return undefined + } + return { + bucket: activeBucket?.name, + uploadIDMarker: markers?.uploadIDMarker || undefined, + keyMarker: markers?.keyMarker || undefined, + limit, + } + }, [activeBucket, limit, markers]) + + const response = useMultipartUploadListUploads({ + disabled: !payload, + payload: payload as MultipartUploadListUploadsPayload, + config: { + swr: { + keepPreviousData: true, + }, + }, + }) + + const nextMarker = useBuildMarkerParam({ + uploadIDMarker: response.data?.nextUploadIDMarker || '', + keyMarker: response.data?.nextMarker || '', + }) + + const datasetPage = useMemo>(() => { + const uploads = maybeFromNullishArrayResponse(response.data?.uploads) + if (!uploads || !activeBucket?.name) { + return undefined + } + return uploads.map((upload) => { + const id = upload.uploadID + const key = upload.key + const name = getFilename(key) + const fullPath = join(activeBucket.name, upload.key) + const localUpload = uploadsMap[getUploadId(fullPath)] + if (localUpload) { + return localUpload + } + return { + id, + path: fullPath, + key, + bucket: activeBucket, + name, + size: 1, + loaded: 1, + multipartId: upload.uploadID, + isUploading: true, + uploadStatus: 'uploading', + createdAt: upload.createdAt, + remote: true, + type: 'file', + uploadAbort: async () => { + await apiBusUploadAbort.post({ + payload: { + bucket: activeBucket?.name, + key: upload.key, + uploadID: upload.uploadID, + }, + }) + }, + } + }) + }, [uploadsMap, activeBucket, response, apiBusUploadAbort]) + + const datasetState = useDatasetState({ + datasetPage, + isValidating: response.isValidating, + error: response.error, + marker, + }) + + const tableState = useTableState('renterd/v0/uploads', { + columns, + columnsDefaultVisible, + sortOptions, + defaultSortField, + }) + + return { + datasetState, + limit, + marker, + nextMarker, + hasMore: !!response.data?.hasMore, + isLoading: response.isLoading, + error: response.error, + datasetPageTotal: datasetPage?.length || 0, + datasetPage, + tableState, + } +} + +function useMarkersFromParam(marker: Nullable) { + return useMemo>(() => { + if (marker) { + const [uploadIDMarker, keyMarker] = marker.split('') + if (uploadIDMarker && keyMarker) { + return { keyMarker, uploadIDMarker } + } + return undefined + } + return undefined + }, [marker]) +} + +function useBuildMarkerParam({ + uploadIDMarker, + keyMarker, +}: { + uploadIDMarker: string + keyMarker: string +}): Nullable { + return useMemo(() => { + if (!uploadIDMarker || !keyMarker) { + return null + } + return `${uploadIDMarker}${keyMarker}` + }, [uploadIDMarker, keyMarker]) +} diff --git a/libs/design-system/src/hooks/useResetPagination.ts b/libs/design-system/src/hooks/useResetPagination.ts index 0ad35436e..db659787f 100644 --- a/libs/design-system/src/hooks/useResetPagination.ts +++ b/libs/design-system/src/hooks/useResetPagination.ts @@ -3,14 +3,6 @@ import { useAppRouter, usePathname, useSearchParams } from '@siafoundation/next' import { useCallback } from 'react' -export type ServerFilterItem = { - id: string - label: string - value?: string - values?: string[] - bool?: boolean -} - export function useResetPagination() { const router = useAppRouter() const pathname = usePathname() diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts index 1804cb811..91983f3c9 100644 --- a/libs/design-system/src/index.ts +++ b/libs/design-system/src/index.ts @@ -174,6 +174,7 @@ export * from './hooks/useSiacoinFiat' export * from './hooks/useOS' export * from './hooks/usePaginationOffset' export * from './hooks/usePaginationMarker' +export * from './hooks/useResetPagination' // multi export * from './multi/useMultiSelect'