Skip to content

Commit

Permalink
feat(renterd): uploads list local and remote
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Jan 19, 2025
1 parent e526e1c commit ecbd835
Show file tree
Hide file tree
Showing 21 changed files with 469 additions and 351 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-onions-stare.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 12 additions & 6 deletions apps/renterd-e2e/src/fixtures/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { maybeExpectAndReturn, step } from '@siafoundation/e2e'
export const uploadInList = step(
'expect upload in list',
async (page: Page, id: string, timeout?: number) => {
await expect(page.getByTestId('uploadsTable').getByTestId(id)).toBeVisible({
await expect(getUploadsTable(page).getByTestId(id)).toBeVisible({
timeout,
})
}
Expand All @@ -13,25 +13,31 @@ export const uploadInList = step(
export const uploadNotInList = step(
'expect upload not in list',
async (page: Page, id: string) => {
await expect(page.getByTestId('uploadsTable').getByTestId(id)).toBeHidden()
await expect(getUploadsTable(page).getByTestId(id)).toBeHidden()
}
)

export const getUploadRowById = step(
'get upload row by ID',
async (page: Page, id: string, shouldExpect?: boolean) => {
return maybeExpectAndReturn(
page.getByTestId('uploadsTable').getByTestId(id),
getUploadsTable(page).getByTestId(id),
shouldExpect
)
}
)

export function getUploadsTable(page: Page) {
return page.getByTestId('uploadsTable')
}

export function getUploadRows(page: Page) {
return getUploadsTable(page).locator('tbody').getByRole('row')
}

export const expectUploadRowById = step(
'expect upload row by ID',
async (page: Page, id: string) => {
return expect(
page.getByTestId('uploadsTable').getByTestId(id)
).toBeVisible()
return expect(getUploadsTable(page).getByTestId(id)).toBeVisible()
}
)
8 changes: 4 additions & 4 deletions apps/renterd-e2e/src/specs/pagination.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ test('paginating files works as expected in both directory and all files mode',
await expect(next).toBeDisabled()
})

test('paginating uploads works as expected', async ({ page }) => {
test('paginating all uploads works as expected', async ({ page }) => {
const bucketName = 'bucket1'
// We use a mock for the multipart uploads API because it is otherwise hard to
// catch a stable list of uploads before they complete and the list clears out.
await mockApiMultipartUploads(page)
await navigateToBuckets({ page })
await createBucket(page, bucketName)
await openBucket(page, bucketName)
const navToUploads = page.getByRole('button', { name: 'Active uploads' })
await expect(navToUploads).toBeVisible()
await navToUploads.click()
await changeExplorerMode(page, 'uploads')
await expect(page.getByRole('button', { name: 'All uploads' })).toBeVisible()
await page.getByRole('button', { name: 'All uploads' }).click()
await expect(page.getByTestId('uploadsTable')).toBeVisible()
const first = page.getByRole('button', { name: 'go to first page' })
const next = page.getByRole('button', { name: 'go to next page' })
Expand Down
65 changes: 65 additions & 0 deletions apps/renterd-e2e/src/specs/uploads.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { test, expect, Page } from '@playwright/test'
import { navigateToBuckets } from '../fixtures/navigate'
import { createBucket, openBucket } from '../fixtures/buckets'
import { changeExplorerMode, createFilesMap } from '../fixtures/files'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { workerMultipartKeyRoute } from '@siafoundation/renterd-types'
import { getUploadRows, getUploadsTable } from '../fixtures/uploads'

test.beforeEach(async ({ page }) => {
await beforeTest(page, {
hostdCount: 3,
})
// Simulate in progress uploads.
await mockApiWorkerMultipartUploadPartHanging({ page })
})

test.afterEach(async () => {
await afterTest()
})

test('uploads are shown in the local and all uploads lists', async ({
page,
}) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
await openBucket(page, bucketName)
await createFilesMap(page, bucketName, {
'file1.txt': 10,
'file2.txt': 10,
'file3.txt': 10,
'file4.txt': 10,
'file5.txt': 10,
'file6.txt': 10,
})
await expect(
page.getByRole('button', { name: 'Active uploads' })
).toBeVisible()
await changeExplorerMode(page, 'uploads')
await expect(getUploadRows(page)).toHaveCount(6)
await expect(getUploadsTable(page).getByText('uploading')).toHaveCount(5)
await expect(getUploadsTable(page).getByText('queued')).toHaveCount(1)
await expect(page.getByText('1 - 6 of 6')).toBeVisible()
await expect(page.getByRole('button', { name: 'All uploads' })).toBeVisible()
await page.getByRole('button', { name: 'All uploads' }).click()
await expect(page.getByText('1 - 6 of 6')).toBeHidden()
await expect(getUploadRows(page)).toHaveCount(5)
await expect(getUploadsTable(page).getByText('uploading')).toHaveCount(5)
await expect(getUploadsTable(page).getByText('queued')).toHaveCount(0)
})

export async function mockApiWorkerMultipartUploadPartHanging({
page,
}: {
page: Page
}) {
await page.route(
`**/api${workerMultipartKeyRoute.replace(':key', '')}*`,
async () => {
await new Promise(() => {
// Never resolve, leaving the request hanging.
})
}
)
}
9 changes: 6 additions & 3 deletions apps/renterd/components/TransfersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { useUploads } from '../contexts/uploads'
export function TransfersBar() {
const { isUnlockedAndAuthedRoute } = useAppSettings()
const { isViewingUploads, navigateToUploads } = useFilesManager()
const { datasetPageTotal: uploadsPageTotal } = useUploads()

const isActiveUploads = !!uploadsPageTotal
const {
localUploads: { datasetTotal: uploadsTotal },
} = useUploads()

const isActiveUploads = !!uploadsTotal

if (!isUnlockedAndAuthedRoute) {
return <AppDockedControl />
Expand All @@ -32,7 +35,7 @@ export function TransfersBar() {
className="flex gap-1"
>
<Upload16 className="opacity-50 scale-75 relative top-px" />
Active uploads
Active uploads ({uploadsTotal.toLocaleString()})
</Button>
</div>
</AppDockedControl>
Expand Down
29 changes: 0 additions & 29 deletions apps/renterd/components/Uploads/EmptyState/StateNoneMatching.tsx

This file was deleted.

29 changes: 0 additions & 29 deletions apps/renterd/components/Uploads/EmptyState/StateNoneYet.tsx

This file was deleted.

27 changes: 0 additions & 27 deletions apps/renterd/components/Uploads/EmptyState/index.tsx

This file was deleted.

47 changes: 47 additions & 0 deletions apps/renterd/components/Uploads/StateNoneYet.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-10 justify-center items-center h-[400px]">
<Text>
<CloudUpload32 className="scale-[200%]" />
</Text>
<div className="flex flex-col gap-3 items-center">
<Text color="subtle" className="text-center max-w-[500px]">
{activeView === 'localUploads' ? (
<>
The <Code>{activeBucket}</Code> bucket does not have any active
uploads from this session.
</>
) : (
<>
The <Code>{activeBucket}</Code> bucket does not have any active
uploads.
</>
)}
</Text>
<LinkButton
href={href}
onClick={(e) => {
e.stopPropagation()
}}
>
{activeBucket ? 'View files' : 'View buckets list'}
</LinkButton>
</div>
</div>
)
}
60 changes: 60 additions & 0 deletions apps/renterd/components/Uploads/UploadsStatsMenu.tsx
Original file line number Diff line number Diff line change
@@ -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' ? (
<PaginatorMarker
marker={remoteUploads.marker}
nextMarker={remoteUploads.nextMarker}
isMore={remoteUploads.hasMore}
limit={remoteUploads.limit}
pageTotal={remoteUploads.datasetPageTotal}
isLoading={remoteUploads.datasetState === 'loading'}
/>
) : (
<PaginatorKnownTotal
offset={localUploads.offset}
limit={localUploads.limit}
total={localUploads.datasetTotal}
isLoading={localUploads.datasetState === 'loading'}
/>
)

return (
<div className="flex gap-3 w-full">
<Button
onClick={() => setActiveView('localUploads')}
variant={activeView === 'localUploads' ? 'active' : 'inactive'}
>
Local uploads
</Button>
<Button
onClick={() => setActiveView('globalUploads')}
variant={activeView === 'globalUploads' ? 'active' : 'inactive'}
>
All uploads
</Button>
<div className="flex-1" />
{activeData.datasetPageTotal > 0 && (
<Button onClick={abortAll}>
Abort ({activeData.datasetPageTotal})
</Button>
)}
{paginatorEl}
</div>
)
}
30 changes: 0 additions & 30 deletions apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx

This file was deleted.

Loading

0 comments on commit ecbd835

Please sign in to comment.