Skip to content

Commit

Permalink
feat: renterd filter files
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Dec 1, 2023
1 parent 56aece3 commit eae7d2f
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-ladybugs-protect.md
@@ -0,0 +1,5 @@
---
'@siafoundation/react-renterd': minor
---

useObjectDirectory now supports prefix filtering.
5 changes: 5 additions & 0 deletions .changeset/short-fishes-beg.md
@@ -0,0 +1,5 @@
---
'renterd': minor
---

File warnings are now a simple warning icon next to the stats and are explained in a popover.
5 changes: 5 additions & 0 deletions .changeset/slow-dancers-fold.md
@@ -0,0 +1,5 @@
---
'renterd': minor
---

Selecting a file search result now navigates to the directory and applies a file name filter.
5 changes: 5 additions & 0 deletions .changeset/smooth-tables-wink.md
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The bucket empty state now has a button for navigating back to the buckets list.
5 changes: 5 additions & 0 deletions .changeset/sweet-pillows-join.md
@@ -0,0 +1,5 @@
---
'renterd': minor
---

Files in the current directory can now be filtered.
22 changes: 18 additions & 4 deletions apps/renterd/components/Files/EmptyState/StateNoneMatching.tsx
@@ -1,15 +1,29 @@
import { Text } from '@siafoundation/design-system'
import { Button, Text } from '@siafoundation/design-system'
import { Filter32 } from '@siafoundation/react-icons'
import { useFiles } from '../../../contexts/files'

export function StateNoneMatching() {
const { filters, resetFilters } = useFiles()
return (
<div className="flex flex-col gap-10 justify-center items-center h-[400px]">
<Text>
<Filter32 className="scale-[200%]" />
</Text>
<Text color="subtle" className="text-center max-w-[500px]">
No files matching filters.
</Text>
<div className="flex flex-col gap-3 items-center">
<Text color="subtle" className="text-center max-w-[500px]">
No files matching filters.
</Text>
{!!filters.length && (
<Button
onClick={(e) => {
e.stopPropagation()
resetFilters()
}}
>
Clear filters
</Button>
)}
</div>
</div>
)
}
22 changes: 18 additions & 4 deletions apps/renterd/components/Files/EmptyState/StateNoneYet.tsx
@@ -1,15 +1,29 @@
import { Text } from '@siafoundation/design-system'
import { Code, LinkButton, Text } from '@siafoundation/design-system'
import { CloudUpload32 } from '@siafoundation/react-icons'
import { routes } from '../../../config/routes'
import { useFiles } from '../../../contexts/files'

export function StateNoneYet() {
const { activeBucket } = useFiles()
return (
<div className="flex flex-col gap-10 justify-center items-center h-[400px] cursor-pointer">
<Text>
<CloudUpload32 className="scale-[200%]" />
</Text>
<Text color="subtle" className="text-center max-w-[500px]">
No files, drag and drop files or click here to start uploading.
</Text>
<div className="flex flex-col gap-3 items-center">
<Text color="subtle" className="text-center max-w-[500px]">
The <Code>{activeBucket}</Code> bucket does not contain any files,
drag and drop files or click here to start uploading.
</Text>
<LinkButton
href={routes.files.index}
onClick={(e) => {
e.stopPropagation()
}}
>
View buckets list
</LinkButton>
</div>
</div>
)
}
35 changes: 34 additions & 1 deletion apps/renterd/components/Files/FileContextMenu/index.tsx
Expand Up @@ -15,17 +15,19 @@ import {
Delete16,
Document16,
Warning16,
Filter16,
} from '@siafoundation/react-icons'
import { useFiles } from '../../../contexts/files'
import { useFileDelete } from '../useFileDelete'
import { CopyMetadataMenuItem } from './CopyMetadataMenuItem'
import { getFilename } from '../../../contexts/files/paths'

type Props = {
path: string
}

export function FileContextMenu({ path }: Props) {
const { downloadFiles, getFileUrl } = useFiles()
const { downloadFiles, getFileUrl, navigateToFile } = useFiles()
const deleteFile = useFileDelete()

return (
Expand Down Expand Up @@ -54,7 +56,38 @@ export function FileContextMenu({ path }: Props) {
</DropdownMenuLeftSlot>
Delete file
</DropdownMenuItem>
<DropdownMenuLabel>Filter</DropdownMenuLabel>
<DropdownMenuItem
onSelect={() => {
navigateToFile(path)
}}
>
<DropdownMenuLeftSlot>
<Filter16 />
</DropdownMenuLeftSlot>
Filter by file name
</DropdownMenuItem>
<DropdownMenuLabel>Copy</DropdownMenuLabel>
<DropdownMenuItem
onSelect={() => {
copyToClipboard(path, 'file path')
}}
>
<DropdownMenuLeftSlot>
<Copy16 />
</DropdownMenuLeftSlot>
Copy file path
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
copyToClipboard(getFilename(path), 'file path')
}}
>
<DropdownMenuLeftSlot>
<Copy16 />
</DropdownMenuLeftSlot>
Copy file name
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
copyToClipboard(getFileUrl(path, false), 'file URL')
Expand Down
12 changes: 3 additions & 9 deletions apps/renterd/components/Files/FilesCmd/FilesSearchCmd/index.tsx
@@ -1,10 +1,7 @@
import { CommandGroup, CommandItemSearch } from '../../../CmdRoot/Item'
import { Page } from '../../../CmdRoot/types'
import { useObjectSearch } from '@siafoundation/react-renterd'
import {
getDirectorySegmentsFromPath,
isDirectory,
} from '../../../../contexts/files/paths'
import { isDirectory } from '../../../../contexts/files/paths'
import { useFiles } from '../../../../contexts/files'
import { Text } from '@siafoundation/design-system'
import { Document16, FolderIcon } from '@siafoundation/react-icons'
Expand All @@ -30,7 +27,7 @@ export function FilesSearchCmd({
beforeSelect?: () => void
afterSelect?: () => void
}) {
const { activeBucket, setActiveDirectory } = useFiles()
const { activeBucket, navigateToFile } = useFiles()
const onSearchPage = currentPage?.namespace === filesSearchPage.namespace
const results = useObjectSearch({
disabled: !onSearchPage,
Expand Down Expand Up @@ -64,10 +61,7 @@ export function FilesSearchCmd({
key={path}
onSelect={() => {
beforeSelect()
setActiveDirectory(() => [
activeBucket,
...getDirectorySegmentsFromPath(path),
])
navigateToFile(path)
afterSelect()
}}
value={path}
Expand Down
56 changes: 56 additions & 0 deletions apps/renterd/components/Files/FilesFilterDirectoryMenu/index.tsx
@@ -0,0 +1,56 @@
import { Button, Separator, TextField } from '@siafoundation/design-system'
import { useEffect, useState } from 'react'
import { useFiles } from '../../../contexts/files'
import { useDebounce } from 'use-debounce'
import { Close16 } from '@siafoundation/react-icons'

export function FilesFilterDirectoryMenu() {
const { filters, setFilter, removeFilter } = useFiles()
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebounce(search, 500)

useEffect(() => {
const fileNamePrefixFilter = filters.find((f) => f.id === 'fileNamePrefix')
const fileNamePrefix = fileNamePrefixFilter?.value || ''
if (fileNamePrefix !== search) {
setSearch(fileNamePrefix)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setSearch, filters])

useEffect(() => {
if (debouncedSearch.length) {
setFilter({
id: 'fileNamePrefix',
label: '',
value: debouncedSearch,
})
} else {
removeFilter('fileNamePrefix')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearch])

console.log('hi')

return (
<div className="flex gap-1 flex-1">
<TextField
variant="ghost"
focus="none"
placeholder="Filter files in current directory"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
className="w-full"
/>
{!!search.length && (
<>
<Button variant="ghost" onClick={() => setSearch('')}>
<Close16 />
</Button>
<Separator variant="vertical" className="h-full" />
</>
)}
</div>
)
}
4 changes: 3 additions & 1 deletion apps/renterd/components/Files/FilesSearchDialog/index.tsx
Expand Up @@ -4,12 +4,14 @@ import { FilesSearchMenu } from '../FilesSearchMenu'
type Props = {
open: boolean
onOpenChange: (open: boolean) => void
trigger?: React.ReactNode
}

export function FilesSearchDialog({ open, onOpenChange }: Props) {
export function FilesSearchDialog({ open, onOpenChange, trigger }: Props) {
return (
<Dialog
open={open}
trigger={trigger}
onOpenChange={onOpenChange}
contentVariants={{
className: '!absolute !p-1 w-[450px] top-[200px]',
Expand Down
@@ -1,36 +1,54 @@
import { Text, Tooltip } from '@siafoundation/design-system'
import {
Button,
Paragraph,
Popover,
Separator,
Text,
} from '@siafoundation/design-system'
import { Warning16 } from '@siafoundation/react-icons'
import { useContractSetMismatch } from '../checks/useContractSetMismatch'
import { useMemo } from 'react'

export function FilesStatsMenuWarnings() {
const contractSetMismatch = useContractSetMismatch()

// warn about contract set mismatch
if (contractSetMismatch.active) {
return (
<Tooltip
align="start"
content={
<>
const contractSetMismatchEl = useMemo(() => {
// warn about contract set mismatch
if (contractSetMismatch.active) {
return (
<div className="flex flex-col gap-2">
<Text size="12" font="mono" weight="medium" color="amber">
Uploaded data will not be managed by autopilot.
</Text>
<Paragraph size="12">
The autopilot contract set does not match the default contract set.
This means that by default workers will not upload data to contracts
that autopilot manages. Unless these contract are being manually
maintained, this will result in data loss. Continue with caution or
update the autopilot contract set to match the default contract set.
</>
}
>
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Uploaded data will not be managed by autopilot.
</Text>
</Paragraph>
</div>
</Tooltip>
)
)
}
return null
}, [contractSetMismatch.active])

if (!contractSetMismatchEl) {
return null
}

return null
return (
<>
<Popover
trigger={
<Button variant="ghost" icon="contrast" color="amber">
<Warning16 />
</Button>
}
>
<div className="px-1 py-2">{contractSetMismatchEl}</div>
</Popover>
<Separator variant="vertical" className="h-full" />
</>
)
}
20 changes: 16 additions & 4 deletions apps/renterd/components/Files/FilesStatsMenu/index.tsx
Expand Up @@ -10,14 +10,26 @@ import { FilesStatsMenuHealth } from './FilesStatsMenuHealth'
import { FilesStatsMenuWarnings } from './FilesStatsMenuWarnings'
import { FilesStatsMenuCount } from './FilesStatsMenuCount'
import { useFiles } from '../../../contexts/files'
import { FilesFilterDirectoryMenu } from '../FilesFilterDirectoryMenu'

export function FilesStatsMenu() {
const { limit, offset, pageCount, dataState, isViewingABucket } = useFiles()
const {
limit,
offset,
pageCount,
dataState,
isViewingABucket,
isViewingBuckets,
} = useFiles()
return (
<div className="flex gap-4 w-full">
<FilesStatsMenuWarnings />
<div className="flex-1" />
<div className="flex gap-3 w-full">
{isViewingBuckets ? (
<div className="flex-1" />
) : (
<FilesFilterDirectoryMenu />
)}
<div className="flex gap-3 items-center">
<FilesStatsMenuWarnings />
<div className="flex gap-3">
<Tooltip side="bottom" content="Filtered statistics">
<Text size="12" color="verySubtle">
Expand Down

0 comments on commit eae7d2f

Please sign in to comment.