Skip to content

Commit

Permalink
feat(renterd): directory mode files multiselect and batch delete
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Oct 24, 2024
1 parent a0316dc commit 44c4c86
Show file tree
Hide file tree
Showing 37 changed files with 668 additions and 222 deletions.
15 changes: 15 additions & 0 deletions apps/hostd/components/ActionsBottomMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { OnboardingBar } from './OnboardingBar'

export function ActionsBottomMenu({
children,
}: {
children?: React.ReactNode
}) {
return (
<div className="flex flex-col gap-2">
{children}
<OnboardingBar />
</div>
)
}
6 changes: 4 additions & 2 deletions apps/hostd/components/HostdAuthedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import { connectivityRoute } from '../config/routes'
import { useSyncStatus } from '../hooks/useSyncStatus'
import { HostdTestnetWarningBanner } from './HostdTestnetWarningBanner'
import { Profile } from './Profile'
import { ActionsBottomMenu } from './ActionsBottomMenu'

type Props = Omit<
React.ComponentProps<typeof AppAuthedLayout>,
'appName' | 'connectivityRoute' | 'walletBalance' | 'profile' | 'isSynced'
>

export function HostdAuthedLayout(props: Props) {
export function HostdAuthedLayout({ actionsBottom, ...props }: Props) {
const wallet = useWallet()
const { isSynced } = useSyncStatus()
return (
<AppAuthedLayout
appName="hostd"
connectivityRoute={connectivityRoute}
banner={<HostdTestnetWarningBanner />}
profile={<Profile />}
banner={<HostdTestnetWarningBanner />}
isSynced={isSynced}
walletBalanceSc={
wallet.data && {
Expand All @@ -29,6 +30,7 @@ export function HostdAuthedLayout(props: Props) {
unconfirmed: new BigNumber(wallet.data.unconfirmed),
}
}
actionsBottom={<ActionsBottomMenu>{actionsBottom}</ActionsBottomMenu>}
{...props}
/>
)
Expand Down
54 changes: 36 additions & 18 deletions apps/hostd/components/OnboardingBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { toHastings } from '@siafoundation/units'
import { useAppSettings } from '@siafoundation/react-core'
import { useVolumes } from '../contexts/volumes'
import useLocalStorageState from 'use-local-storage-state'
import { AnimatePresence, motion } from 'framer-motion'

export function OnboardingBar() {
const { isUnlockedAndAuthedRoute } = useAppSettings()
Expand Down Expand Up @@ -61,13 +62,13 @@ export function OnboardingBar() {
const totalSteps = steps.length
const completedSteps = steps.filter((step) => step).length

if (totalSteps === completedSteps) {
return null
}
let el = null

if (maximized) {
return (
<div className="z-20 fixed bottom-5 right-5 flex justify-center">
if (totalSteps === completedSteps) {
el = null
} else if (maximized) {
el = (
<div className="flex justify-center">
<Panel className="w-[400px] flex flex-col max-h-[600px]">
<ScrollArea>
<div className="flex justify-between items-center px-3 py-2 border-b border-gray-200 dark:border-graydark-300">
Expand Down Expand Up @@ -230,20 +231,37 @@ export function OnboardingBar() {
</Panel>
</div>
)
} else {
el = (
<div className="flex justify-center">
<Button
onClick={() => setMaximized(true)}
size="large"
className="flex gap-3 !px-3"
>
<Text className="flex items-center gap-1">
<Logo />
Setup: {completedSteps}/{totalSteps} steps complete
</Text>
</Button>
</div>
)
}

return (
<div className="z-30 fixed bottom-5 right-5 flex justify-center">
<Button
onClick={() => setMaximized(true)}
size="large"
className="flex gap-3 !px-3"
>
<Text className="flex items-center gap-1">
<Logo />
Setup: {completedSteps}/{totalSteps} steps complete
</Text>
</Button>
</div>
<AnimatePresence>
{el && (
<motion.div
className="pointer-events-auto"
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ duration: 0.2 }}
>
{el}
</motion.div>
)}
</AnimatePresence>
)
}

Expand Down
1 change: 1 addition & 0 deletions apps/hostd/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function App(props: AppPropsWithLayout) {
</NextAppCsr>
)
}

function AppCore({ Component, pageProps }: AppPropsWithLayout) {
const Layout = Component.Layout
const layoutProps = Component.useLayoutProps()
Expand Down
97 changes: 94 additions & 3 deletions apps/renterd-e2e/src/fixtures/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Page, expect } from '@playwright/test'
import { readFileSync } from 'fs'
import { fillTextInputByName } from '@siafoundation/e2e'
import { navigateToBuckets } from './navigate'
import { openBucket } from './buckets'
import { join } from 'path'

export async function deleteFile(page: Page, path: string) {
await openFileContextMenu(page, path)
Expand Down Expand Up @@ -62,12 +65,26 @@ export async function openFileContextMenu(page: Page, path: string) {
}

export async function openDirectory(page: Page, path: string) {
await page.getByTestId('filesTable').getByTestId(path).click()
const parts = path.split('/')
const name = parts[parts.length - 2] + '/'
await page.getByTestId('filesTable').getByTestId(path).getByText(name).click()
for (const dir of path.split('/').slice(0, -1)) {
await expect(page.getByTestId('navbar').getByText(dir)).toBeVisible()
}
}

export async function openDirectoryFromAnywhere(page: Page, path: string) {
const bucket = path.split('/')[0]
const dirParts = path.split('/').slice(1)
await navigateToBuckets({ page })
await openBucket(page, path.split('/')[0])
let currentPath = bucket + '/'
for (const dir of dirParts) {
currentPath += dir + '/'
await openDirectory(page, currentPath)
}
}

export async function navigateToParentDirectory(page: Page) {
const isEmpty = await page
.getByText('The current directory does not contain any files yet')
Expand Down Expand Up @@ -107,11 +124,11 @@ export async function fileNotInList(page: Page, path: string) {
await expect(page.getByTestId('filesTable').getByTestId(path)).toBeHidden()
}

export async function getFileRowById(page: Page, id: string) {
export function getFileRowById(page: Page, id: string) {
return page.getByTestId('filesTable').getByTestId(id)
}

export async function dragAndDropFile(
async function simulateDragAndDropFile(
page: Page,
selector: string,
filePath: string,
Expand Down Expand Up @@ -139,3 +156,77 @@ export async function dragAndDropFile(

await page.dispatchEvent(selector, 'drop', { dataTransfer })
}

export async function dragAndDropFileFromSystem(
page: Page,
systemFilePath: string,
localFileName?: string
) {
await simulateDragAndDropFile(
page,
`[data-testid=filesDropzone]`,
join(__dirname, 'sample-files', systemFilePath),
'/' + (localFileName || systemFilePath)
)
}

export interface FileMap {
[key: string]: string | FileMap
}

// Iterate through the file map and create files/directories.
export async function createFilesMap(
page: Page,
bucketName: string,
map: FileMap
) {
const create = async (map: FileMap, stack: string[]) => {
for (const name in map) {
await openDirectoryFromAnywhere(page, stack.join('/'))
const currentDirPath = stack.join('/')
const path = `${currentDirPath}/${name}`
if (typeof map[name] === 'object') {
await createDirectory(page, name)
await fileInList(page, path + '/')
await create(map[name] as FileMap, stack.concat(name))
} else {
await dragAndDropFileFromSystem(page, 'sample.txt', name)
await fileInList(page, path)
}
}
}
await create(map, [bucketName])
await navigateToBuckets({ page })
await openBucket(page, bucketName)
}

interface FileExpectMap {
[key: string]: 'visible' | 'hidden' | FileExpectMap
}

// Check each file and directory in the map exists.
export async function expectFilesMap(
page: Page,
bucketName: string,
map: FileExpectMap
) {
const check = async (map: FileMap, stack: string[]) => {
for (const name in map) {
await openDirectoryFromAnywhere(page, stack.join('/'))
const currentDirPath = stack.join('/')
const path = `${currentDirPath}/${name}`
if (typeof map[name] === 'string') {
const state = map[name] as 'visible' | 'hidden'
if (state === 'visible') {
await fileInList(page, path)
} else {
await fileNotInList(page, path)
}
} else {
await fileInList(page, path + '/')
await check(map[name] as FileMap, stack.concat(name))
}
}
}
await check(map, [bucketName])
}
File renamed without changes.
71 changes: 50 additions & 21 deletions apps/renterd-e2e/src/specs/files.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { test, expect } from '@playwright/test'
import { navigateToBuckets } from '../fixtures/navigate'
import { createBucket, deleteBucket, openBucket } from '../fixtures/buckets'
import path from 'path'
import {
deleteDirectory,
deleteFile,
dragAndDropFile,
fileInList,
fileNotInList,
getFileRowById,
navigateToParentDirectory,
openDirectory,
openFileContextMenu,
createDirectory,
dragAndDropFileFromSystem,
createFilesMap,
expectFilesMap,
} from '../fixtures/files'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { clearToasts, fillTextInputByName } from '@siafoundation/e2e'
Expand Down Expand Up @@ -81,12 +82,7 @@ test('can create directory, upload file, rename file, navigate, delete a file, d
await clearToasts({ page })

// Upload.
await dragAndDropFile(
page,
`[data-testid=filesDropzone]`,
path.join(__dirname, originalFileName),
originalFileName
)
await dragAndDropFileFromSystem(page, originalFileName)
await expect(page.getByText('100%')).toBeVisible()
await fileInList(page, originalFilePath)

Expand All @@ -104,12 +100,7 @@ test('can create directory, upload file, rename file, navigate, delete a file, d
await clearToasts({ page })

// Upload the file again.
await dragAndDropFile(
page,
`[data-testid=filesDropzone]`,
path.join(__dirname, originalFileName),
originalFileName
)
await dragAndDropFileFromSystem(page, originalFileName)
await expect(page.getByText('100%')).toBeVisible()
await fileInList(page, originalFilePath)

Expand All @@ -131,7 +122,7 @@ test('shows a new intermediate directory when uploading nested files', async ({
const bucketName = 'files-test'
const containerDir = 'test-dir'
const containerDirPath = `${bucketName}/${containerDir}/`
const systemDir = 'nested-sample'
const systemDir = 'sample-files'
const systemFile = 'sample.txt'
const systemFilePath = `${systemDir}/${systemFile}`
const dirPath = `${bucketName}/${containerDir}/${systemDir}/`
Expand All @@ -154,12 +145,7 @@ test('shows a new intermediate directory when uploading nested files', async ({
await clearToasts({ page })

// Upload a nested file.
await dragAndDropFile(
page,
`[data-testid=filesDropzone]`,
path.join(__dirname, systemFilePath),
'/' + systemFilePath
)
await dragAndDropFileFromSystem(page, systemFile, systemFilePath)
await fileInList(page, dirPath)
const dirRow = await getFileRowById(page, dirPath)
// The intermediate directory should show up before the file is finished uploading.
Expand Down Expand Up @@ -188,3 +174,46 @@ test('shows a new intermediate directory when uploading nested files', async ({
await navigateToBuckets({ page })
await deleteBucket(page, bucketName)
})

test('batch delete across nested directories', async ({ page }) => {
test.setTimeout(120_000)
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
await createFilesMap(page, bucketName, {
dir1: {
'file1.txt': null,
'file2.txt': null,
},
dir2: {
'file3.txt': null,
'file4.txt': null,
'file5.txt': null,
},
})
await navigateToBuckets({ page })
await openBucket(page, bucketName)

// Select entire dir1.
await getFileRowById(page, 'bucket1/dir1/').click()
await openDirectory(page, 'bucket1/dir2/')

// Select file3 and file4.
await getFileRowById(page, 'bucket1/dir2/file3.txt').click()
await getFileRowById(page, 'bucket1/dir2/file4.txt').click()
const menu = page.getByLabel('file multiselect menu')

// Delete selected files.
await menu.getByLabel('delete selected files').click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('button', { name: 'Delete' }).click()

await expectFilesMap(page, bucketName, {
'dir1/': 'hidden',
dir2: {
'file3.txt': 'hidden',
'file4.txt': 'hidden',
'file5.txt': 'visible',
},
})
})
Loading

0 comments on commit 44c4c86

Please sign in to comment.