From 44c4c868036a071b6c32b8b74e3febe5ffc52886 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Fri, 18 Oct 2024 11:44:49 -0400 Subject: [PATCH] feat(renterd): directory mode files multiselect and batch delete --- apps/hostd/components/ActionsBottomMenu.tsx | 15 ++ apps/hostd/components/HostdAuthedLayout.tsx | 6 +- apps/hostd/components/OnboardingBar.tsx | 54 ++++--- apps/hostd/pages/_app.tsx | 1 + apps/renterd-e2e/src/fixtures/files.ts | 97 ++++++++++- .../sample-files}/sample.txt | 0 .../src/{specs => fixtures}/sample.txt | 0 apps/renterd-e2e/src/specs/files.spec.ts | 71 +++++--- apps/renterd/components/ActionsBottomMenu.tsx | 17 ++ .../components/Files/BucketContextMenu.tsx | 2 +- .../components/Files/DirectoryContextMenu.tsx | 1 + .../Files/FileContextMenu/index.tsx | 15 +- apps/renterd/components/Files/Layout.tsx | 8 +- .../FilesDirectoryActionsBottomMenu.tsx | 5 + .../FilesDirectoryBatchDelete.tsx | 77 +++++++++ .../FilesDirectoryBatchMenu/index.tsx | 13 ++ ...u.tsx => FilesDirectoryBreadcrumbMenu.tsx} | 4 +- .../FilesDirectory/FilesExplorer.tsx | 2 + .../components/FilesFlat/FilesExplorer.tsx | 4 +- .../components/Keys/KeysActionsBottomMenu.tsx | 5 + apps/renterd/components/Keys/Layout.tsx | 2 + apps/renterd/components/Keys/index.tsx | 2 - apps/renterd/components/OnboardingBar.tsx | 54 ++++--- .../components/RenterdAuthedLayout.tsx | 8 +- apps/renterd/components/TransfersBar.tsx | 153 ++++++++++-------- apps/renterd/config/providers.tsx | 4 - .../contexts/filesDirectory/columns.tsx | 31 +++- .../renterd/contexts/filesDirectory/index.tsx | 77 +++++++-- apps/renterd/contexts/filesFlat/index.tsx | 18 ++- .../renterd/contexts/filesManager/dataset.tsx | 15 +- apps/renterd/contexts/filesManager/types.ts | 24 ++- apps/renterd/pages/_app.tsx | 1 + apps/walletd/components/ActionsBottomMenu.tsx | 9 ++ .../components/WalletdAuthedLayout.tsx | 4 +- apps/walletd/pages/_app.tsx | 1 + .../src/app/AppAuthedLayout/index.tsx | 5 + .../src/multi/MultiSelectionMenu.tsx | 85 +++++----- 37 files changed, 668 insertions(+), 222 deletions(-) create mode 100644 apps/hostd/components/ActionsBottomMenu.tsx rename apps/renterd-e2e/src/{specs/nested-sample => fixtures/sample-files}/sample.txt (100%) rename apps/renterd-e2e/src/{specs => fixtures}/sample.txt (100%) create mode 100644 apps/renterd/components/ActionsBottomMenu.tsx create mode 100644 apps/renterd/components/FilesDirectory/FilesDirectoryActionsBottomMenu.tsx create mode 100644 apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/FilesDirectoryBatchDelete.tsx create mode 100644 apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/index.tsx rename apps/renterd/components/FilesDirectory/{FilesBreadcrumbMenu.tsx => FilesDirectoryBreadcrumbMenu.tsx} (96%) create mode 100644 apps/renterd/components/Keys/KeysActionsBottomMenu.tsx create mode 100644 apps/walletd/components/ActionsBottomMenu.tsx diff --git a/apps/hostd/components/ActionsBottomMenu.tsx b/apps/hostd/components/ActionsBottomMenu.tsx new file mode 100644 index 000000000..877b81384 --- /dev/null +++ b/apps/hostd/components/ActionsBottomMenu.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { OnboardingBar } from './OnboardingBar' + +export function ActionsBottomMenu({ + children, +}: { + children?: React.ReactNode +}) { + return ( +
+ {children} + +
+ ) +} diff --git a/apps/hostd/components/HostdAuthedLayout.tsx b/apps/hostd/components/HostdAuthedLayout.tsx index 7dd9f1bdd..5617b8000 100644 --- a/apps/hostd/components/HostdAuthedLayout.tsx +++ b/apps/hostd/components/HostdAuthedLayout.tsx @@ -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, 'appName' | 'connectivityRoute' | 'walletBalance' | 'profile' | 'isSynced' > -export function HostdAuthedLayout(props: Props) { +export function HostdAuthedLayout({ actionsBottom, ...props }: Props) { const wallet = useWallet() const { isSynced } = useSyncStatus() return ( } profile={} + banner={} isSynced={isSynced} walletBalanceSc={ wallet.data && { @@ -29,6 +30,7 @@ export function HostdAuthedLayout(props: Props) { unconfirmed: new BigNumber(wallet.data.unconfirmed), } } + actionsBottom={{actionsBottom}} {...props} /> ) diff --git a/apps/hostd/components/OnboardingBar.tsx b/apps/hostd/components/OnboardingBar.tsx index 21b9a54f2..4ec23724b 100644 --- a/apps/hostd/components/OnboardingBar.tsx +++ b/apps/hostd/components/OnboardingBar.tsx @@ -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() @@ -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 ( -
+ if (totalSteps === completedSteps) { + el = null + } else if (maximized) { + el = ( +
@@ -230,20 +231,37 @@ export function OnboardingBar() {
) + } else { + el = ( +
+ +
+ ) } + return ( -
- -
+ + {el && ( + + {el} + + )} + ) } diff --git a/apps/hostd/pages/_app.tsx b/apps/hostd/pages/_app.tsx index 3bcc5f40d..5bd615516 100644 --- a/apps/hostd/pages/_app.tsx +++ b/apps/hostd/pages/_app.tsx @@ -33,6 +33,7 @@ export default function App(props: AppPropsWithLayout) { ) } + function AppCore({ Component, pageProps }: AppPropsWithLayout) { const Layout = Component.Layout const layoutProps = Component.useLayoutProps() diff --git a/apps/renterd-e2e/src/fixtures/files.ts b/apps/renterd-e2e/src/fixtures/files.ts index 3f036901e..64e582a71 100644 --- a/apps/renterd-e2e/src/fixtures/files.ts +++ b/apps/renterd-e2e/src/fixtures/files.ts @@ -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) @@ -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') @@ -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, @@ -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]) +} diff --git a/apps/renterd-e2e/src/specs/nested-sample/sample.txt b/apps/renterd-e2e/src/fixtures/sample-files/sample.txt similarity index 100% rename from apps/renterd-e2e/src/specs/nested-sample/sample.txt rename to apps/renterd-e2e/src/fixtures/sample-files/sample.txt diff --git a/apps/renterd-e2e/src/specs/sample.txt b/apps/renterd-e2e/src/fixtures/sample.txt similarity index 100% rename from apps/renterd-e2e/src/specs/sample.txt rename to apps/renterd-e2e/src/fixtures/sample.txt diff --git a/apps/renterd-e2e/src/specs/files.spec.ts b/apps/renterd-e2e/src/specs/files.spec.ts index 5acc452a1..e4227dc10 100644 --- a/apps/renterd-e2e/src/specs/files.spec.ts +++ b/apps/renterd-e2e/src/specs/files.spec.ts @@ -1,11 +1,9 @@ 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, @@ -13,6 +11,9 @@ import { openDirectory, openFileContextMenu, createDirectory, + dragAndDropFileFromSystem, + createFilesMap, + expectFilesMap, } from '../fixtures/files' import { afterTest, beforeTest } from '../fixtures/beforeTest' import { clearToasts, fillTextInputByName } from '@siafoundation/e2e' @@ -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) @@ -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) @@ -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}/` @@ -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. @@ -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', + }, + }) +}) diff --git a/apps/renterd/components/ActionsBottomMenu.tsx b/apps/renterd/components/ActionsBottomMenu.tsx new file mode 100644 index 000000000..9ba967f55 --- /dev/null +++ b/apps/renterd/components/ActionsBottomMenu.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { OnboardingBar } from './OnboardingBar' +import { TransfersBar } from './TransfersBar' + +export function ActionsBottomMenu({ + children, +}: { + children?: React.ReactNode +}) { + return ( +
+ {children} + + +
+ ) +} diff --git a/apps/renterd/components/Files/BucketContextMenu.tsx b/apps/renterd/components/Files/BucketContextMenu.tsx index b2c49d579..1b2f0cc3f 100644 --- a/apps/renterd/components/Files/BucketContextMenu.tsx +++ b/apps/renterd/components/Files/BucketContextMenu.tsx @@ -17,7 +17,7 @@ export function BucketContextMenu({ name }: Props) { return ( + } diff --git a/apps/renterd/components/Files/DirectoryContextMenu.tsx b/apps/renterd/components/Files/DirectoryContextMenu.tsx index 8106875e7..19e632cda 100644 --- a/apps/renterd/components/Files/DirectoryContextMenu.tsx +++ b/apps/renterd/components/Files/DirectoryContextMenu.tsx @@ -23,6 +23,7 @@ export function DirectoryContextMenu({ path, size }: Props) { trigger={ ) } - contentProps={{ align: 'start', ...contentProps }} + contentProps={{ + align: 'start', + ...contentProps, + onClick: (e) => { + e.stopPropagation() + }, + }} > Actions openDialog('settings'), nav: activeMode === 'directory' ? ( - + ) : ( ), stats: , actions: , + actionsBottom: activeMode === 'directory' && ( + + ), } } diff --git a/apps/renterd/components/FilesDirectory/FilesDirectoryActionsBottomMenu.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryActionsBottomMenu.tsx new file mode 100644 index 000000000..2ddee58c4 --- /dev/null +++ b/apps/renterd/components/FilesDirectory/FilesDirectoryActionsBottomMenu.tsx @@ -0,0 +1,5 @@ +import { FilesDirectoryBatchMenu } from './FilesDirectoryBatchMenu' + +export function FilesDirectoryActionsBottomMenu() { + return +} diff --git a/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/FilesDirectoryBatchDelete.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/FilesDirectoryBatchDelete.tsx new file mode 100644 index 000000000..7e385e6d2 --- /dev/null +++ b/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/FilesDirectoryBatchDelete.tsx @@ -0,0 +1,77 @@ +import { + Button, + Paragraph, + triggerSuccessToast, + triggerErrorToast, +} from '@siafoundation/design-system' +import { Delete16 } from '@siafoundation/react-icons' +import { useCallback, useMemo } from 'react' +import { useDialog } from '../../../contexts/dialog' +import { useFilesDirectory } from '../../../contexts/filesDirectory' +import { useObjectsRemove } from '@siafoundation/renterd-react' + +export function FilesDirectoryBatchDelete() { + const { multiSelect } = useFilesDirectory() + + const filesToDelete = useMemo( + () => + Object.entries(multiSelect.selectionMap).map(([_, item]) => ({ + bucket: item.bucket.name, + prefix: item.key, + })), + [multiSelect.selectionMap] + ) + const { openConfirmDialog } = useDialog() + const objectsRemove = useObjectsRemove() + const deleteFiles = useCallback(async () => { + const totalCount = filesToDelete.length + let errorCount = 0 + for (const { bucket, prefix } of filesToDelete) { + const response = await objectsRemove.post({ + payload: { + bucket, + prefix, + }, + }) + if (response.error) { + errorCount++ + } + } + if (errorCount > 0) { + triggerErrorToast({ + title: `${totalCount - errorCount} files deleted`, + body: `Error deleting ${errorCount}/${totalCount} total files.`, + }) + } else { + triggerSuccessToast({ title: `${totalCount} files deleted` }) + } + multiSelect.deselectAll() + }, [multiSelect, filesToDelete, objectsRemove]) + + return ( + + ) +} diff --git a/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/index.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/index.tsx new file mode 100644 index 000000000..74f8a33b7 --- /dev/null +++ b/apps/renterd/components/FilesDirectory/FilesDirectoryBatchMenu/index.tsx @@ -0,0 +1,13 @@ +import { MultiSelectionMenu } from '@siafoundation/design-system' +import { FilesDirectoryBatchDelete } from './FilesDirectoryBatchDelete' +import { useFilesDirectory } from '../../../contexts/filesDirectory' + +export function FilesDirectoryBatchMenu() { + const { multiSelect } = useFilesDirectory() + + return ( + + + + ) +} diff --git a/apps/renterd/components/FilesDirectory/FilesBreadcrumbMenu.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryBreadcrumbMenu.tsx similarity index 96% rename from apps/renterd/components/FilesDirectory/FilesBreadcrumbMenu.tsx rename to apps/renterd/components/FilesDirectory/FilesDirectoryBreadcrumbMenu.tsx index b6d251a12..d5e98ba8a 100644 --- a/apps/renterd/components/FilesDirectory/FilesBreadcrumbMenu.tsx +++ b/apps/renterd/components/FilesDirectory/FilesDirectoryBreadcrumbMenu.tsx @@ -4,7 +4,7 @@ import { ChevronRight16 } from '@siafoundation/react-icons' import { useFilesManager } from '../../contexts/filesManager' import { FilesExplorerModeButton } from '../Files/FilesExplorerModeButton' -export function FilesBreadcrumbMenu() { +export function FilesDirectoryBreadcrumbMenu() { const { activeDirectory, setActiveDirectory } = useFilesManager() const ref = useRef(null) @@ -29,7 +29,7 @@ export function FilesBreadcrumbMenu() { className="flex items-center cursor-pointer" noWrap > - Buckets + Buckets22 {activeDirectory.length > 0 && ( diff --git a/apps/renterd/components/FilesDirectory/FilesExplorer.tsx b/apps/renterd/components/FilesDirectory/FilesExplorer.tsx index e4085279a..f08d79a56 100644 --- a/apps/renterd/components/FilesDirectory/FilesExplorer.tsx +++ b/apps/renterd/components/FilesDirectory/FilesExplorer.tsx @@ -18,6 +18,7 @@ export function FilesExplorer() { datasetPage, pageCount, dataState, + cellContext, onDragEnd, onDragOver, onDragStart, @@ -40,6 +41,7 @@ export function FilesExplorer() { emptyState={} pageSize={10} data={datasetPage} + context={cellContext} columns={columns} sortableColumns={sortableColumns} sortField={sortField} diff --git a/apps/renterd/components/FilesFlat/FilesExplorer.tsx b/apps/renterd/components/FilesFlat/FilesExplorer.tsx index 39e1e7fc7..35d06a2e8 100644 --- a/apps/renterd/components/FilesFlat/FilesExplorer.tsx +++ b/apps/renterd/components/FilesFlat/FilesExplorer.tsx @@ -6,7 +6,8 @@ import { columns } from '../../contexts/filesFlat/columns' export function FilesExplorer() { const { sortableColumns, toggleSort } = useFilesManager() - const { datasetPage, dataState, sortField, sortDirection } = useFilesFlat() + const { datasetPage, dataState, cellContext, sortField, sortDirection } = + useFilesFlat() return (
} pageSize={10} data={datasetPage} + context={cellContext} columns={columns} sortableColumns={sortableColumns} sortField={sortField} diff --git a/apps/renterd/components/Keys/KeysActionsBottomMenu.tsx b/apps/renterd/components/Keys/KeysActionsBottomMenu.tsx new file mode 100644 index 000000000..95bb8a5be --- /dev/null +++ b/apps/renterd/components/Keys/KeysActionsBottomMenu.tsx @@ -0,0 +1,5 @@ +import { KeysBatchMenu } from './KeysBatchMenu' + +export function KeysActionsBottomMenu() { + return +} diff --git a/apps/renterd/components/Keys/Layout.tsx b/apps/renterd/components/Keys/Layout.tsx index 83e3b1a5b..6eb2eacdb 100644 --- a/apps/renterd/components/Keys/Layout.tsx +++ b/apps/renterd/components/Keys/Layout.tsx @@ -7,6 +7,7 @@ import { } from '../RenterdAuthedLayout' import { KeysActionsMenu } from './KeysActionsMenu' import { KeysStatsMenu } from './KeysStatsMenu' +import { KeysActionsBottomMenu } from './KeysActionsBottomMenu' export const Layout = RenterdAuthedLayout export function useLayoutProps(): RenterdAuthedPageLayoutProps { @@ -18,5 +19,6 @@ export function useLayoutProps(): RenterdAuthedPageLayoutProps { openSettings: () => openDialog('settings'), actions: , stats: , + actionsBottom: , } } diff --git a/apps/renterd/components/Keys/index.tsx b/apps/renterd/components/Keys/index.tsx index 59b88784c..e4dcc0758 100644 --- a/apps/renterd/components/Keys/index.tsx +++ b/apps/renterd/components/Keys/index.tsx @@ -3,7 +3,6 @@ import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' import { StateError } from './StateError' import { useKeys } from '../../contexts/keys' -import { KeysBatchMenu } from './KeysBatchMenu' export function Keys() { const { @@ -20,7 +19,6 @@ export function Keys() { return (
-
step).length - if (totalSteps === completedSteps) { - return null - } + let el = null - if (maximized) { - return ( -
+ if (totalSteps === completedSteps) { + el = null + } else if (maximized) { + el = ( +
@@ -237,20 +238,37 @@ export function OnboardingBar() {
) + } else { + el = ( +
+ +
+ ) } + return ( -
- -
+ + {el && ( + + {el} + + )} + ) } diff --git a/apps/renterd/components/RenterdAuthedLayout.tsx b/apps/renterd/components/RenterdAuthedLayout.tsx index c9d6a21b1..a1490ea11 100644 --- a/apps/renterd/components/RenterdAuthedLayout.tsx +++ b/apps/renterd/components/RenterdAuthedLayout.tsx @@ -5,30 +5,32 @@ import { connectivityRoute } from '../config/routes' import { useSyncStatus } from '../hooks/useSyncStatus' import { Profile } from './Profile' import { RenterdTestnetWarningBanner } from './RenterdTestnetWarningBanner' +import { ActionsBottomMenu } from './ActionsBottomMenu' type Props = Omit< React.ComponentProps, 'appName' | 'connectivityRoute' | 'walletBalance' | 'profile' | 'isSynced' > -export function RenterdAuthedLayout(props: Props) { +export function RenterdAuthedLayout({ actionsBottom, ...props }: Props) { const wallet = useWallet() const { isSynced } = useSyncStatus() return ( } banner={} - connectivityRoute={connectivityRoute} isSynced={isSynced} walletBalanceSc={ wallet.data && { spendable: new BigNumber(wallet.data.spendable), confirmed: new BigNumber(wallet.data.confirmed), - unconfirmed: new BigNumber(wallet.data.unconfirmed), immature: new BigNumber(wallet.data.immature), + unconfirmed: new BigNumber(wallet.data.unconfirmed), } } + actionsBottom={{actionsBottom}} {...props} /> ) diff --git a/apps/renterd/components/TransfersBar.tsx b/apps/renterd/components/TransfersBar.tsx index 8ff48bfaa..030d28c40 100644 --- a/apps/renterd/components/TransfersBar.tsx +++ b/apps/renterd/components/TransfersBar.tsx @@ -5,6 +5,7 @@ import { useFilesManager } from '../contexts/filesManager' import { useAppSettings } from '@siafoundation/react-core' import { TransfersBarItem } from './TransfersBarItem' import { useUploads } from '../contexts/uploads' +import { AnimatePresence, motion } from 'framer-motion' export function TransfersBar() { const { isUnlockedAndAuthedRoute } = useAppSettings() @@ -17,80 +18,92 @@ export function TransfersBar() { const downloadCount = downloadsList.length const isActiveDownloads = !!downloadCount - if (!isUnlockedAndAuthedRoute) { - return null - } - - if (!isActiveUploads && !isActiveDownloads) { - return null - } + let el = null - const controls = ( -
- {isActiveUploads && !isViewingUploads ? ( - - ) : null} - {isActiveDownloads ? ( - - ) : null} -
- ) - - if (isActiveDownloads && maximized) { - return ( -
- - - {isActiveDownloads ? ( - <> -
- - Active downloads ({downloadCount}) - - -
- {downloadsList.map((download) => ( - downloadCancel(download)} - abortTip="Cancel download" - /> - ))} - - ) : null} -
-
- {controls} + if (!isUnlockedAndAuthedRoute) { + el = null + } else if (!isActiveUploads && !isActiveDownloads) { + el = null + } else { + const controls = ( +
+ {isActiveUploads && !isViewingUploads ? ( + + ) : null} + {isActiveDownloads ? ( + + ) : null}
) + + el = controls + + if (isActiveDownloads && maximized) { + el = ( +
+ + + {isActiveDownloads ? ( + <> +
+ + Active downloads ({downloadCount}) + + +
+ {downloadsList.map((download) => ( + downloadCancel(download)} + abortTip="Cancel download" + /> + ))} + + ) : null} +
+
+ {controls} +
+ ) + } } + return ( -
- {controls} -
+ + {el && ( + + {el} + + )} + ) } diff --git a/apps/renterd/config/providers.tsx b/apps/renterd/config/providers.tsx index 413a68929..87fd107f3 100644 --- a/apps/renterd/config/providers.tsx +++ b/apps/renterd/config/providers.tsx @@ -4,8 +4,6 @@ import { ContractsProvider } from '../contexts/contracts' import { HostsProvider } from '../contexts/hosts' import { AppProvider } from '../contexts/app' import { ConfigProvider } from '../contexts/config' -import { OnboardingBar } from '../components/OnboardingBar' -import { TransfersBar } from '../components/TransfersBar' import { TransactionsProvider } from '../contexts/transactions' import { KeysProvider } from '../contexts/keys' import { FilesFlatProvider } from '../contexts/filesFlat' @@ -34,8 +32,6 @@ export function Providers({ children }: Props) { {/* this is here so that dialogs can use all the other providers, and the other providers can trigger dialogs */} - - {children} diff --git a/apps/renterd/contexts/filesDirectory/columns.tsx b/apps/renterd/contexts/filesDirectory/columns.tsx index b901d8132..089d7f227 100644 --- a/apps/renterd/contexts/filesDirectory/columns.tsx +++ b/apps/renterd/contexts/filesDirectory/columns.tsx @@ -1,4 +1,11 @@ -import { Button, Text, Tooltip, ValueNum } from '@siafoundation/design-system' +import { + Button, + Checkbox, + ControlGroup, + Text, + Tooltip, + ValueNum, +} from '@siafoundation/design-system' import { Document16, Earth16, @@ -20,14 +27,28 @@ export const columns: FilesTableColumn[] = [ id: 'type', label: '', fixed: true, - cellClassName: 'w-[50px] !pl-2 !pr-2 [&+*]:!pl-0', + contentClassName: '!pl-3 !pr-4', + cellClassName: 'w-[20px] !pl-0 !pr-0', + heading: ({ context: { isViewingBuckets, multiSelect } }) => { + if (isViewingBuckets) { + return null + } + return ( + + + + ) + }, render: function TypeColumn({ data: { isUploading, type, name, path, size }, }) { const { setActiveDirectory } = useFilesManager() if (isUploading) { return ( - ) @@ -35,6 +56,7 @@ export const columns: FilesTableColumn[] = [ if (name === '..') { return (
diff --git a/libs/design-system/src/multi/MultiSelectionMenu.tsx b/libs/design-system/src/multi/MultiSelectionMenu.tsx index cf772a385..2ef80b17c 100644 --- a/libs/design-system/src/multi/MultiSelectionMenu.tsx +++ b/libs/design-system/src/multi/MultiSelectionMenu.tsx @@ -21,50 +21,49 @@ export function MultiSelectionMenu({ }) { const isVisible = multiSelect.selectionCount > 0 return ( -
- - {isVisible && ( - + {isVisible && ( + + - + {`${pluralize(multiSelect.selectionCount, entityWord, { + plural: entityWordPlural, + })} selected${ + multiSelect.someSelectedItemsOutsideCurrentPage && + multiSelect.someSelectedOnCurrentPage + ? ' on this and other pages' + : !multiSelect.someSelectedItemsOutsideCurrentPage && + multiSelect.someSelectedOnCurrentPage + ? '' + : multiSelect.someSelectedItemsOutsideCurrentPage && + !multiSelect.someSelectedOnCurrentPage + ? ' on other pages' + : '' + }`} + + )} +
+ {children} + - - - )} - -
+ + +
+
+ )} +
) }