mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
Merge pull request #11438 from supabase/chore/storage-download-multiple-files-as-zip
Chore/storage download multiple files as zip
This commit is contained in:
14 files changed
+377
-230
No files matched your search
Generated
+16
@@ -11040,6 +11040,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.6.61",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.6.61.tgz",
|
||||
"integrity": "sha512-3zBN4zSTXUTz0vFzSnVV76a1tNVBj5cH6rSRDGpYzY95P6JQkepN0puXgtUqLHMTsnfGbwnpG1yVO93RH3xQ1g==",
|
||||
"engines": {
|
||||
"deno": ">=1.0.0",
|
||||
"node": ">=16.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -37585,6 +37594,7 @@
|
||||
"@supabase/supabase-js": "^2.1.1",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-query-devtools": "^4.16.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
@@ -45664,6 +45674,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.6.61",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.6.61.tgz",
|
||||
"integrity": "sha512-3zBN4zSTXUTz0vFzSnVV76a1tNVBj5cH6rSRDGpYzY95P6JQkepN0puXgtUqLHMTsnfGbwnpG1yVO93RH3xQ1g=="
|
||||
},
|
||||
"abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -63391,6 +63406,7 @@
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"autoprefixer": "10.4.2",
|
||||
|
||||
@@ -12,9 +12,9 @@ interface ReportsLayoutProps {
|
||||
|
||||
const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps>) => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const project = ui.selectedProject
|
||||
const page = router.pathname.split('/')[4] || ''
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ import { WizardLayoutWithoutAuth } from './WizardLayout'
|
||||
import VercelIntegrationLayout from './VercelIntegrationLayout'
|
||||
import BillingLayout from './BillingLayout'
|
||||
import LogsLayout from './LogsLayout/LogsLayout'
|
||||
import ReportsLayout from './ReportsLayout/ReportsLayout'
|
||||
import OrganizationLayout from './OrganizationLayout'
|
||||
|
||||
export {
|
||||
@@ -35,6 +36,7 @@ export {
|
||||
VercelIntegrationLayout,
|
||||
BillingLayout,
|
||||
LogsLayout,
|
||||
ReportsLayout,
|
||||
OrganizationLayout,
|
||||
}
|
||||
|
||||
|
||||
+78
-40
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { FC, useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { compact, debounce, isEqual } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
IconList,
|
||||
} from 'ui'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants.ts'
|
||||
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants'
|
||||
|
||||
const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
|
||||
const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }: any) => {
|
||||
return (
|
||||
<div
|
||||
className={`group ${!loading ? 'cursor-pointer' : ''}`}
|
||||
@@ -46,11 +46,11 @@ const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }: any) => {
|
||||
// Max 5 crumbs, otherwise replace middle segment with ellipsis and only
|
||||
// have the first 2 and last 2 crumbs visible
|
||||
const ellipsis = '...'
|
||||
const breadcrumbsWithIndexes = breadcrumbs.map((name, index) => {
|
||||
const breadcrumbsWithIndexes = breadcrumbs.map((name: string, index: number) => {
|
||||
return { name, index }
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-3 flex items-center">
|
||||
{formattedBreadcrumbs.map((crumb, idx) => (
|
||||
{formattedBreadcrumbs.map((crumb: any, idx: number) => (
|
||||
<div className="flex items-center" key={crumb.name}>
|
||||
{idx !== 0 && <IconChevronRight size={10} strokeWidth={2} className="mx-3" />}
|
||||
<p
|
||||
@@ -91,37 +91,52 @@ const HeaderBreadcrumbs = ({ loading, breadcrumbs, selectBreadcrumb }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const FileExplorerHeader = ({
|
||||
view = STORAGE_VIEWS.COLUMNS,
|
||||
sortBy = STORAGE_SORT_BY.NAME,
|
||||
sortByOrder = STORAGE_SORT_BY_ORDER.ASC,
|
||||
loading = {},
|
||||
breadcrumbs = [],
|
||||
backDisabled = false,
|
||||
isSearching = false,
|
||||
interface Props {
|
||||
isSearching: boolean
|
||||
itemSearchString: string
|
||||
setIsSearching: (value: boolean) => void
|
||||
setItemSearchString: (value: string) => void
|
||||
onFilesUpload: (event: any, columnIndex: number) => void
|
||||
}
|
||||
|
||||
const FileExplorerHeader: FC<Props> = ({
|
||||
itemSearchString = '',
|
||||
setItemSearchString = () => {},
|
||||
onSetPathByString = () => {},
|
||||
onChangeView = () => {},
|
||||
onChangeSortBy = () => {},
|
||||
onChangeSortByOrder = () => {},
|
||||
onToggleSearch = () => {},
|
||||
onFilesUpload = () => {},
|
||||
onSelectBack = () => {},
|
||||
onSelectCreateFolder = () => {},
|
||||
onSelectBreadcrumb = () => {},
|
||||
}) => {
|
||||
const debounceDuration = 300
|
||||
const [isEditingPath, setIsEditingPath] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [pathString, setPathString] = useState('')
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [loading, setLoading] = useState({ isLoading: false, message: '' })
|
||||
|
||||
const uploadButtonRef = useRef(null)
|
||||
const previousBreadcrumbs = useRef(null)
|
||||
const [isEditingPath, setIsEditingPath] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
|
||||
const uploadButtonRef: any = useRef(null)
|
||||
const previousBreadcrumbs: any = useRef(null)
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { refetchAllOpenedFolders } = storageExplorerStore
|
||||
const {
|
||||
view,
|
||||
setView,
|
||||
columns,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
sortByOrder,
|
||||
setSortByOrder,
|
||||
popColumn,
|
||||
popColumnAtIndex,
|
||||
popOpenedFolders,
|
||||
fetchFoldersByPath,
|
||||
refetchAllOpenedFolders,
|
||||
addNewFolderPlaceholder,
|
||||
clearOpenedFolders,
|
||||
closeFilePreview,
|
||||
} = storageExplorerStore
|
||||
|
||||
const breadcrumbs = columns.map((column: any) => column.name)
|
||||
const backDisabled = columns.length <= 1
|
||||
|
||||
useEffect(() => {
|
||||
if (itemSearchString) setSearchString(itemSearchString)
|
||||
@@ -137,11 +152,18 @@ const FileExplorerHeader = ({
|
||||
}, [breadcrumbs])
|
||||
|
||||
const searchInputHandler = useCallback(debounce(setItemSearchString, debounceDuration), [])
|
||||
const onSearchInputUpdate = (event) => {
|
||||
const onSearchInputUpdate = (event: any) => {
|
||||
setSearchString(event.target.value)
|
||||
// @ts-ignore
|
||||
searchInputHandler(event.target.value)
|
||||
}
|
||||
|
||||
const onSelectBack = () => {
|
||||
popColumn()
|
||||
popOpenedFolders()
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
const onSelectUpload = () => {
|
||||
if (uploadButtonRef.current) {
|
||||
uploadButtonRef.current.click()
|
||||
@@ -155,11 +177,11 @@ const FileExplorerHeader = ({
|
||||
if (isSearching) onCancelSearch()
|
||||
}
|
||||
|
||||
const onUpdatePathString = (event) => {
|
||||
const onUpdatePathString = (event: any) => {
|
||||
setPathString(event.target.value)
|
||||
}
|
||||
|
||||
const navigateByPathString = (event) => {
|
||||
const navigateByPathString = (event: any) => {
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -168,6 +190,19 @@ const FileExplorerHeader = ({
|
||||
onSetPathByString(compact(pathString.split('/')))
|
||||
}
|
||||
|
||||
const onSetPathByString = async (paths: any[]) => {
|
||||
if (paths.length === 0) {
|
||||
popColumnAtIndex(0)
|
||||
clearOpenedFolders()
|
||||
closeFilePreview()
|
||||
} else {
|
||||
const pathString = paths.join('/')
|
||||
setLoading({ isLoading: true, message: `Navigating to ${pathString}...` })
|
||||
await fetchFoldersByPath(paths)
|
||||
setLoading({ isLoading: false, message: '' })
|
||||
}
|
||||
}
|
||||
|
||||
const cancelSetPathString = () => {
|
||||
setIsEditingPath(false)
|
||||
}
|
||||
@@ -177,17 +212,19 @@ const FileExplorerHeader = ({
|
||||
// Searching for column view requires much more thinking
|
||||
const toggleSearch = () => {
|
||||
setIsEditingPath(false)
|
||||
onToggleSearch(true)
|
||||
setIsSearching(true)
|
||||
}
|
||||
|
||||
const onCancelSearch = () => {
|
||||
setSearchString('')
|
||||
onToggleSearch(false)
|
||||
setIsSearching(false)
|
||||
setItemSearchString('')
|
||||
}
|
||||
|
||||
/** Methods for breadcrumbs */
|
||||
|
||||
const selectBreadcrumb = (columnIndex) => {
|
||||
onSelectBreadcrumb(columnIndex)
|
||||
const selectBreadcrumb = (columnIndex: number) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
@@ -278,7 +315,7 @@ const FileExplorerHeader = ({
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlay={[
|
||||
<Dropdown.RadioGroup key="viewOptions" value={view} onChange={onChangeView}>
|
||||
<Dropdown.RadioGroup key="viewOptions" value={view} onChange={setView}>
|
||||
<Dropdown.Radio value={STORAGE_VIEWS.COLUMNS}>Columns</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_VIEWS.LIST}>List</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>,
|
||||
@@ -295,14 +332,14 @@ const FileExplorerHeader = ({
|
||||
}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onChange={onChangeView}
|
||||
onChange={setView}
|
||||
>
|
||||
View as
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
overlay={[
|
||||
<Dropdown.RadioGroup key="sortOptions" value={sortBy} onChange={onChangeSortBy}>
|
||||
<Dropdown.RadioGroup key="sortOptions" value={sortBy} onChange={setSortBy}>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.NAME}>Name</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.CREATED_AT}>Time created</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY.UPDATED_AT}>Time modified</Dropdown.Radio>
|
||||
@@ -326,7 +363,7 @@ const FileExplorerHeader = ({
|
||||
<Dropdown.RadioGroup
|
||||
key="sortOrderOptions"
|
||||
value={sortByOrder}
|
||||
onChange={onChangeSortByOrder}
|
||||
onChange={setSortByOrder}
|
||||
>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY_ORDER.ASC}>Ascending</Dropdown.Radio>
|
||||
<Dropdown.Radio value={STORAGE_SORT_BY_ORDER.DESC}>Descending</Dropdown.Radio>
|
||||
@@ -352,6 +389,7 @@ const FileExplorerHeader = ({
|
||||
<div className="h-6 border-r border-panel-border-light dark:border-panel-border-dark" />
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="hidden">
|
||||
{/* @ts-ignore */}
|
||||
<input ref={uploadButtonRef} type="file" multiple onChange={onFilesUpload} />
|
||||
</div>
|
||||
<Button
|
||||
@@ -366,7 +404,7 @@ const FileExplorerHeader = ({
|
||||
icon={<IconFolderPlus size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onClick={() => onSelectCreateFolder()}
|
||||
onClick={() => addNewFolderPlaceholder(-1)}
|
||||
>
|
||||
Create folder
|
||||
</Button>
|
||||
@@ -381,7 +419,7 @@ const FileExplorerHeader = ({
|
||||
<Input
|
||||
size="tiny"
|
||||
autoFocus
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
icon={<IconSearch size={'tiny'} strokeWidth={2} />}
|
||||
actions={[
|
||||
<IconX
|
||||
+22
-12
@@ -1,31 +1,41 @@
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { Button, IconDownload, IconTrash2, IconMove, IconX } from 'ui'
|
||||
|
||||
const FileExplorerHeaderSelection = ({
|
||||
selectedItems = [],
|
||||
onSelectItemsDownload = () => {},
|
||||
onSelectItemsDelete = () => {},
|
||||
onSelectItemsMove = () => {},
|
||||
onUnselectAllItems = () => {},
|
||||
}) => {
|
||||
const FileExplorerHeaderSelection = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedItems,
|
||||
downloadFile,
|
||||
downloadSelectedFiles,
|
||||
clearSelectedItems,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedItemsToMove,
|
||||
} = storageExplorerStore
|
||||
|
||||
return (
|
||||
<div className="z-10 flex h-[40px] items-center rounded-t-md bg-brand-700 px-2 py-1 shadow dark:bg-brand-600">
|
||||
<Button
|
||||
icon={<IconX size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
shadow={false}
|
||||
onClick={onUnselectAllItems}
|
||||
onClick={() => clearSelectedItems()}
|
||||
/>
|
||||
<div className="ml-4 flex items-center space-x-3">
|
||||
<p className="mb-0 text-sm text-scale-1200">
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{selectedItems.length}</span> items
|
||||
selected
|
||||
</p>
|
||||
{/* [Joshen] Disabled multi download for now */}
|
||||
<Button
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsDownload}
|
||||
onClick={async () => {
|
||||
if (selectedItems.length === 1) {
|
||||
await downloadFile(selectedItems[0])
|
||||
} else {
|
||||
await downloadSelectedFiles(selectedItems)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
@@ -34,7 +44,7 @@ const FileExplorerHeaderSelection = ({
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsDelete}
|
||||
onClick={() => setSelectedItemsToDelete(selectedItems)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -42,7 +52,7 @@ const FileExplorerHeaderSelection = ({
|
||||
icon={<IconMove size={16} strokeWidth={2} />}
|
||||
type="primary"
|
||||
shadow={false}
|
||||
onClick={onSelectItemsMove}
|
||||
onClick={() => setSelectedItemsToMove(selectedItems)}
|
||||
>
|
||||
Move
|
||||
</Button>
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
CONTEXT_MENU_KEYS,
|
||||
} from '../Storage.constants.ts'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const RowIcon = ({ view, status, fileType, mimeType }) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
@@ -85,6 +86,9 @@ const FileExplorerRow = ({
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
}) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder } = storageExplorerStore
|
||||
|
||||
const itemWithColumnIndex = { ...item, columnIndex }
|
||||
const isSelected = find(selectedItems, item) !== undefined
|
||||
const isOpened =
|
||||
@@ -146,8 +150,22 @@ const FileExplorerRow = ({
|
||||
? [{ name: 'Delete', onClick: () => onSelectItemDelete(itemWithColumnIndex) }]
|
||||
: item.type === STORAGE_ROW_TYPES.FOLDER
|
||||
? [
|
||||
{ name: 'Rename', onClick: () => onSelectItemRename(itemWithColumnIndex) },
|
||||
{ name: 'Delete', onClick: () => onSelectItemDelete(itemWithColumnIndex) },
|
||||
{
|
||||
name: 'Rename',
|
||||
icon: <IconEdit size="tiny" />,
|
||||
onClick: () => onSelectItemRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => downloadFolder(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator' },
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash size="tiny" />,
|
||||
onClick: () => onSelectItemDelete(itemWithColumnIndex),
|
||||
},
|
||||
]
|
||||
: [
|
||||
...(!item.isCorrupted
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { Menu, Item } from 'react-contexify'
|
||||
import 'react-contexify/dist/ReactContexify.css'
|
||||
|
||||
const FolderContextMenu = ({ id = '', onRenameFolder = () => {}, onDeleteFolder = () => {} }) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder } = storageExplorerStore
|
||||
|
||||
return (
|
||||
<Menu id={id} animation="fade">
|
||||
<Item onClick={({ props }) => onRenameFolder(props.item)}>Rename</Item>
|
||||
<Item onClick={({ props }) => downloadFolder(props.item)}>Download</Item>
|
||||
<Item onClick={({ props }) => onDeleteFolder(props.item)}>Delete</Item>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
+17
-15
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
IconX,
|
||||
@@ -11,13 +12,13 @@ import SVG from 'react-inlinesvg'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import Link from 'next/link'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: string }) => {
|
||||
if (!mimeType || !previewUrl) {
|
||||
return (
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -36,7 +37,6 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -80,7 +80,6 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
return (
|
||||
<SVG
|
||||
src={'/img/file-filled.svg'}
|
||||
alt={'No preview'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -88,14 +87,17 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const PreviewPane = ({
|
||||
isOpen = false,
|
||||
file = {},
|
||||
width = 400,
|
||||
onCopyFileURL = () => {},
|
||||
onSelectFileDelete = () => {},
|
||||
onClosePreviewPane = () => {},
|
||||
}) => {
|
||||
const PreviewPane = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedFilePreview: file,
|
||||
copyFileURLToClipboard,
|
||||
closeFilePreview,
|
||||
setSelectedItemsToDelete,
|
||||
} = storageExplorerStore
|
||||
|
||||
const width = 450
|
||||
const isOpen = !isEmpty(file)
|
||||
const size = file.metadata ? formatBytes(file.metadata.size) : null
|
||||
const mimeType = file.metadata ? file.metadata.mimetype : null
|
||||
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
|
||||
@@ -123,7 +125,7 @@ const PreviewPane = ({
|
||||
className="cursor-pointer"
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
onClick={onClosePreviewPane}
|
||||
onClick={() => closeFilePreview()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +184,7 @@ const PreviewPane = ({
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
onClick={() => onCopyFileURL(file)}
|
||||
onClick={async () => await copyFileURLToClipboard(file)}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Copy URL
|
||||
@@ -193,7 +195,7 @@ const PreviewPane = ({
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => onSelectFileDelete(file)}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Delete file
|
||||
</Button>
|
||||
@@ -27,10 +27,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
clearSelectedItemsToDelete,
|
||||
openedFolders,
|
||||
pushOpenedFolderAtIndex,
|
||||
clearOpenedFolders,
|
||||
popColumn,
|
||||
popColumnAtIndex,
|
||||
popOpenedFolders,
|
||||
popOpenedFoldersAtIndex,
|
||||
setSelectedItemToRename,
|
||||
selectedItemsToMove,
|
||||
@@ -38,9 +35,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
clearSelectedItemsToMove,
|
||||
view,
|
||||
setView,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
sortByOrder,
|
||||
setSortByOrder,
|
||||
currentBucketName,
|
||||
copyFileURLToClipboard,
|
||||
@@ -51,27 +46,22 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
addNewFolder,
|
||||
fetchFolderContents,
|
||||
fetchMoreFolderContents,
|
||||
fetchFoldersByPath,
|
||||
renameFolder,
|
||||
deleteFolder,
|
||||
uploadFiles,
|
||||
deleteFiles,
|
||||
downloadFile,
|
||||
downloadSelectedFiles,
|
||||
renameFile,
|
||||
moveFiles,
|
||||
} = storageExplorerStore
|
||||
|
||||
const storageExplorerRef = useRef(null)
|
||||
const [loading, setLoading] = useState({ isLoading: false, message: '' })
|
||||
|
||||
// This state exists outside of the header because FileExplorerColumn needs to listen to these as well
|
||||
// I'm keeping them outside of the mobx store as I feel that the store should contain persistent data
|
||||
// Things like showing results from a search filter is "temporary", hence we use react state to manage
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [itemSearchString, setItemSearchString] = useState('')
|
||||
|
||||
const previewPaneWidth = 450
|
||||
// Requires a fixed height to ensure that explorer is constrained to the viewport
|
||||
const fileExplorerHeight = window.innerHeight - 122
|
||||
|
||||
@@ -121,24 +111,12 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
|
||||
const onSelectFolder = async (columnIndex, folder) => {
|
||||
closeFilePreview()
|
||||
clearSelectedItems(columnIndex + 1)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
pushOpenedFolderAtIndex(folder, columnIndex)
|
||||
await fetchFolderContents(folder.id, folder.name, columnIndex)
|
||||
}
|
||||
|
||||
const onSetPathByString = async (paths) => {
|
||||
if (paths.length === 0) {
|
||||
popColumnAtIndex(0)
|
||||
clearOpenedFolders()
|
||||
closeFilePreview()
|
||||
} else {
|
||||
const pathString = paths.join('/')
|
||||
setLoading({ isLoading: true, message: `Navigating to ${pathString}...` })
|
||||
await fetchFoldersByPath(paths)
|
||||
setLoading({ isLoading: false, message: '' })
|
||||
}
|
||||
}
|
||||
|
||||
/** Checkbox selection methods */
|
||||
/** [Joshen] We'll only support checkbox selection for files ONLY */
|
||||
|
||||
@@ -152,9 +130,11 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
}
|
||||
|
||||
const onSelectAllItemsInColumn = (columnIndex) => {
|
||||
const columnFiles = columns[columnIndex].items.filter(
|
||||
(item) => item.type === STORAGE_ROW_TYPES.FILE
|
||||
)
|
||||
const columnFiles = columns[columnIndex].items
|
||||
.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
|
||||
.map((item) => {
|
||||
return { ...item, columnIndex }
|
||||
})
|
||||
const columnFilesId = compact(columnFiles.map((item) => item.id))
|
||||
const selectedItemsFromColumn = selectedItems.filter((item) => columnFilesId.includes(item.id))
|
||||
|
||||
@@ -183,10 +163,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
renameFolder(folder, newName, columnIndex)
|
||||
}
|
||||
|
||||
const onSelectItemsDelete = () => {
|
||||
setSelectedItemsToDelete(selectedItems)
|
||||
}
|
||||
|
||||
const onSelectItemDelete = (file) => {
|
||||
setSelectedItemsToDelete([file])
|
||||
}
|
||||
@@ -195,18 +171,10 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
setSelectedItemToRename(file)
|
||||
}
|
||||
|
||||
const onSelectItemsMove = () => {
|
||||
setSelectedItemsToMove(selectedItems)
|
||||
}
|
||||
|
||||
const onSelectItemMove = (file) => {
|
||||
setSelectedItemsToMove([file])
|
||||
}
|
||||
|
||||
const onSelectItemsDownload = async () => {
|
||||
await downloadSelectedFiles()
|
||||
}
|
||||
|
||||
const onCopyFileURL = async (file) => {
|
||||
await copyFileURLToClipboard(file)
|
||||
}
|
||||
@@ -250,17 +218,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
}
|
||||
|
||||
/** Misc UI methods */
|
||||
|
||||
const onSelectBack = () => {
|
||||
popColumn()
|
||||
popOpenedFolders()
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
const onSelectBreadcrumb = (columnIndex) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
}
|
||||
|
||||
const onSelectColumnEmptySpace = (columnIndex) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
@@ -274,13 +231,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
|
||||
const onChangeSortByOrder = (sortByOrder) => setSortByOrder(sortByOrder)
|
||||
|
||||
const onToggleSearch = (bool) => {
|
||||
setIsSearching(bool)
|
||||
if (bool === false) {
|
||||
setItemSearchString('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={storageExplorerRef}
|
||||
@@ -291,33 +241,12 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
>
|
||||
{selectedItems.length === 0 ? (
|
||||
<FileExplorerHeader
|
||||
view={view}
|
||||
sortBy={sortBy}
|
||||
sortByOrder={sortByOrder}
|
||||
loading={loading}
|
||||
breadcrumbs={columns.map((column) => column.name)}
|
||||
backDisabled={columns.length <= 1}
|
||||
isSearching={isSearching}
|
||||
itemSearchString={itemSearchString}
|
||||
setItemSearchString={setItemSearchString}
|
||||
onChangeView={onChangeView}
|
||||
onChangeSortBy={onChangeSortBy}
|
||||
onChangeSortByOrder={onChangeSortByOrder}
|
||||
onToggleSearch={onToggleSearch}
|
||||
onFilesUpload={onFilesUpload}
|
||||
onSelectBack={onSelectBack}
|
||||
onSelectCreateFolder={onSelectCreateFolder}
|
||||
onSetPathByString={onSetPathByString}
|
||||
onSelectBreadcrumb={onSelectBreadcrumb}
|
||||
/>
|
||||
) : (
|
||||
<FileExplorerHeaderSelection
|
||||
selectedItems={selectedItems}
|
||||
onSelectItemsDownload={onSelectItemsDownload}
|
||||
onSelectItemsDelete={onSelectItemsDelete}
|
||||
onSelectItemsMove={onSelectItemsMove}
|
||||
onUnselectAllItems={clearSelectedItems}
|
||||
/>
|
||||
<FileExplorerHeaderSelection />
|
||||
)}
|
||||
<div className="flex h-full" style={{ height: fileExplorerHeight }}>
|
||||
<FileExplorer
|
||||
@@ -348,14 +277,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
fetchMoreFolderContents(index, column, itemSearchString)
|
||||
}
|
||||
/>
|
||||
<PreviewPane
|
||||
isOpen={!isEmpty(selectedFilePreview)}
|
||||
file={selectedFilePreview}
|
||||
width={previewPaneWidth}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onSelectFileDelete={onSelectItemDelete}
|
||||
onClosePreviewPane={closeFilePreview}
|
||||
/>
|
||||
<PreviewPane />
|
||||
</div>
|
||||
<ConfirmDeleteModal
|
||||
visible={selectedItemsToDelete.length > 0}
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { createContext, useContext } from 'react'
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import {
|
||||
find,
|
||||
compact,
|
||||
isEqual,
|
||||
isNull,
|
||||
isNil,
|
||||
isUndefined,
|
||||
has,
|
||||
some,
|
||||
chunk,
|
||||
get,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import toast from 'react-hot-toast'
|
||||
import { find, compact, isEqual, isNil, has, some, chunk, get, uniq } from 'lodash'
|
||||
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
|
||||
import {
|
||||
STORAGE_VIEWS,
|
||||
STORAGE_ROW_TYPES,
|
||||
STORAGE_ROW_STATUS,
|
||||
STORAGE_SORT_BY,
|
||||
} from 'components/to-be-cleaned/Storage/Storage.constants.ts'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
|
||||
|
||||
/**
|
||||
* This is a preferred method rather than React Context and useStorageExplorerStore().
|
||||
@@ -50,7 +39,6 @@ const LIMIT = 200
|
||||
const OFFSET = 0
|
||||
const DEFAULT_EXPIRY = 10 * 365 * 24 * 60 * 60 // in seconds, default to 1 year
|
||||
const PREVIEW_SIZE_LIMIT = 10000000 // 10MB
|
||||
// const BATCH_SIZE = 10
|
||||
const BATCH_SIZE = 2
|
||||
const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder'
|
||||
const STORAGE_PROGRESS_INFO_TEXT = "Please do not close the browser until it's completed"
|
||||
@@ -288,8 +276,12 @@ class StorageExplorerStore {
|
||||
this.selectedItems = items
|
||||
}
|
||||
|
||||
clearSelectedItems = () => {
|
||||
this.selectedItems = []
|
||||
clearSelectedItems = (columnIndex) => {
|
||||
if (columnIndex !== undefined) {
|
||||
this.selectedItems = this.selectedItems.filter((item) => item.columnIndex !== columnIndex)
|
||||
} else {
|
||||
this.selectedItems = []
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItemsToDelete = (items) => {
|
||||
@@ -319,10 +311,8 @@ class StorageExplorerStore {
|
||||
addNewFolder = async (folderName, columnIndex) => {
|
||||
const autofix = false
|
||||
const formattedName = this.sanitizeNameForDuplicateInColumn(folderName, autofix, columnIndex)
|
||||
if (formattedName === null) return
|
||||
|
||||
if (isNull(formattedName)) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* todo: move this to a util file, as renameFolder() uses same logic
|
||||
*/
|
||||
@@ -374,7 +364,7 @@ class StorageExplorerStore {
|
||||
|
||||
const fetchedAt = get(cachedPreview, ['fetchedAt'], null)
|
||||
const expiresIn = get(cachedPreview, ['expiresIn'], null)
|
||||
const existsInCache = !isNull(fetchedAt) && !isNull(expiresIn)
|
||||
const existsInCache = fetchedAt !== null && expiresIn !== null
|
||||
const isExpired = existsInCache ? fetchedAt + expiresIn * 1000 < Date.now() : true
|
||||
|
||||
if (!isExpired) {
|
||||
@@ -556,7 +546,10 @@ class StorageExplorerStore {
|
||||
|
||||
// https://stackoverflow.com/a/53058574
|
||||
getFilesDataTransferItems = async (items) => {
|
||||
const toastId = toast.loading('Retrieving items to upload...')
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: 'Retrieving items to upload...',
|
||||
})
|
||||
const files = []
|
||||
const queue = []
|
||||
for (const item of items) {
|
||||
@@ -566,7 +559,7 @@ class StorageExplorerStore {
|
||||
const entry = queue.shift() || {}
|
||||
if (entry.isFile) {
|
||||
const file = await this.getFile(entry)
|
||||
if (!isUndefined(file)) {
|
||||
if (file !== undefined) {
|
||||
file.path = entry.fullPath.slice(1)
|
||||
files.push(file)
|
||||
}
|
||||
@@ -637,9 +630,9 @@ class StorageExplorerStore {
|
||||
return file
|
||||
})
|
||||
|
||||
const uploadedTopLevelFolders = []
|
||||
this.uploadProgress = 0
|
||||
let numberOfFilesToUpload = formattedFilesToUpload.length
|
||||
const uploadedTopLevelFolders = []
|
||||
const numberOfFilesToUpload = formattedFilesToUpload.length
|
||||
let numberOfFilesUploadedSuccess = 0
|
||||
let numberOfFilesUploadedFail = 0
|
||||
|
||||
@@ -746,28 +739,34 @@ class StorageExplorerStore {
|
||||
if (numberOfFilesToUpload === 0) {
|
||||
toast.dismiss(toastId)
|
||||
} else if (numberOfFilesUploadedFail === numberOfFilesToUpload) {
|
||||
toast.error(
|
||||
`Failed to upload ${numberOfFilesToUpload} file${numberOfFilesToUpload > 1 ? 's' : ''}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Failed to upload ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
})
|
||||
} else if (numberOfFilesUploadedSuccess === numberOfFilesToUpload) {
|
||||
toast.success(
|
||||
`Successfully uploaded ${numberOfFilesToUpload} file${
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully uploaded ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
})
|
||||
} else {
|
||||
toast.success(
|
||||
`Successfully uploaded ${numberOfFilesUploadedSuccess} out of ${numberOfFilesToUpload} file${
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully uploaded ${numberOfFilesUploadedSuccess} out of ${numberOfFilesToUpload} file${
|
||||
numberOfFilesToUpload > 1 ? 's' : ''
|
||||
}!`,
|
||||
{ id: toastId }
|
||||
)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
error: e,
|
||||
message: 'Failed to upload files',
|
||||
category: 'error',
|
||||
})
|
||||
@@ -943,33 +942,153 @@ class StorageExplorerStore {
|
||||
}
|
||||
}
|
||||
|
||||
downloadSelectedFiles = async () => {
|
||||
const showIndividualToast = false
|
||||
const toastId = toast.loading(`Retrieving ${this.selectedItems.length} files...`, {
|
||||
autoClose: false,
|
||||
hideProgressBar: true,
|
||||
downloadFolder = async (folder) => {
|
||||
let progress = 0
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: 'Retrieving files from folder...',
|
||||
})
|
||||
|
||||
const res = await Promise.all(
|
||||
this.selectedItems.map(async (item) => await this.downloadFile(item, showIndividualToast))
|
||||
)
|
||||
|
||||
const numberOfSuccessfullyDownloadedFiles = res.filter((x) => x === true).length
|
||||
toast.success(`Downloaded ${numberOfSuccessfullyDownloadedFiles} files`, {
|
||||
const files = await this.getAllItemsAlongFolder(folder)
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} files...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
const promises = files.map((file) => {
|
||||
const fileMimeType = file.metadata?.mimetype ?? null
|
||||
return () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const { data, error } = await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.download(`${file.prefix}/${file.name}`)
|
||||
|
||||
progress = progress + 1 / files.length
|
||||
|
||||
if (!error) {
|
||||
resolve({
|
||||
name: file.name,
|
||||
prefix: file.prefix,
|
||||
blob: new Blob([data], { type: fileMimeType }),
|
||||
})
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const batchedPromises = chunk(promises, 10)
|
||||
const downloadedFiles = await batchedPromises.reduce(async (previousPromise, nextBatch) => {
|
||||
const previousResults = await previousPromise
|
||||
const batchResults = await Promise.allSettled(nextBatch.map((batch) => batch()))
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} file${files.length > 1 ? 's' : ''}...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: progress * 100,
|
||||
})
|
||||
return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean))
|
||||
}, Promise.resolve())
|
||||
|
||||
const zipFileWriter = new BlobWriter('application/zip')
|
||||
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
|
||||
downloadedFiles.forEach((file) => {
|
||||
zipWriter.add(`${file.prefix}/${file.name}`, new BlobReader(file.blob))
|
||||
})
|
||||
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close())
|
||||
const link = document.createElement('a')
|
||||
link.href = blobURL
|
||||
link.setAttribute('download', `${folder.name}.zip`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.parentNode.removeChild(link)
|
||||
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully downloaded folder "${folder.name}"`,
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile = async (file, showToast = true) => {
|
||||
let toastId
|
||||
downloadSelectedFiles = async (files) => {
|
||||
const lowestColumnIndex = Math.min(...files.map((file) => file.columnIndex))
|
||||
|
||||
const formattedFilesWithPrefix = files.map((file) => {
|
||||
const { name, columnIndex } = file
|
||||
const pathToFile = this.openedFolders
|
||||
.slice(lowestColumnIndex, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${name}` : name
|
||||
return { ...file, formattedPathToFile }
|
||||
})
|
||||
|
||||
let progress = 0
|
||||
const returnBlob = true
|
||||
const showIndividualToast = false
|
||||
const toastId = this.ui.setNotification({
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} files...`,
|
||||
})
|
||||
|
||||
const promises = formattedFilesWithPrefix.map((file) => {
|
||||
return () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const data = await this.downloadFile(file, showIndividualToast, returnBlob)
|
||||
progress = progress + 1 / formattedFilesWithPrefix.length
|
||||
resolve({ ...data, name: file.formattedPathToFile })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const batchedPromises = chunk(promises, 10)
|
||||
const downloadedFiles = await batchedPromises.reduce(async (previousPromise, nextBatch) => {
|
||||
const previousResults = await previousPromise
|
||||
const batchResults = await Promise.allSettled(nextBatch.map((batch) => batch()))
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'loading',
|
||||
message: `Downloading ${files.length} file${files.length > 1 ? 's' : ''}...`,
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
progress: progress * 100,
|
||||
})
|
||||
return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean))
|
||||
}, Promise.resolve())
|
||||
|
||||
const zipFileWriter = new BlobWriter('application/zip')
|
||||
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
|
||||
downloadedFiles.forEach((file) => {
|
||||
zipWriter.add(file.name, new BlobReader(file.blob))
|
||||
})
|
||||
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close())
|
||||
const link = document.createElement('a')
|
||||
link.href = blobURL
|
||||
link.setAttribute('download', `supabase-files.zip`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.parentNode.removeChild(link)
|
||||
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully downloaded ${downloadedFiles.length} files`,
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile = async (file, showToast = true, returnBlob = false) => {
|
||||
const fileName = file.name
|
||||
const fileMimeType = get(file, ['metadata', 'mimetype'], null)
|
||||
|
||||
if (showToast) {
|
||||
toastId = toast.loading(`Retrieving ${fileName}...`, {
|
||||
autoClose: false,
|
||||
})
|
||||
}
|
||||
const toastId = showToast
|
||||
? this.ui.setNotification({ category: 'loading', message: `Retrieving ${fileName}...` })
|
||||
: undefined
|
||||
|
||||
const pathToFile = this.openedFolders
|
||||
.slice(0, file.columnIndex)
|
||||
@@ -984,8 +1103,10 @@ class StorageExplorerStore {
|
||||
if (!error) {
|
||||
const blob = data
|
||||
const newBlob = new Blob([blob], { type: fileMimeType })
|
||||
const blobUrl = window.URL.createObjectURL(newBlob)
|
||||
|
||||
if (returnBlob) return { name: fileName, blob: newBlob }
|
||||
|
||||
const blobUrl = window.URL.createObjectURL(newBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', `${fileName}`)
|
||||
@@ -994,16 +1115,20 @@ class StorageExplorerStore {
|
||||
link.parentNode.removeChild(link)
|
||||
window.URL.revokeObjectURL(blob)
|
||||
if (toastId) {
|
||||
toast.success(`Downloading ${fileName}`, {
|
||||
this.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Downloading ${fileName}`,
|
||||
})
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to download:', fileName)
|
||||
if (toastId) {
|
||||
toast.error(`Failed to download ${fileName}`, {
|
||||
this.ui.setNotification({
|
||||
error,
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Failed to download ${fileName}`,
|
||||
})
|
||||
}
|
||||
return false
|
||||
@@ -1333,7 +1458,7 @@ class StorageExplorerStore {
|
||||
let formattedPathToFolder = ''
|
||||
const { name, columnIndex, prefix } = folder
|
||||
|
||||
if (isUndefined(prefix)) {
|
||||
if (prefix === undefined) {
|
||||
const pathToFolder = this.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
@@ -1361,8 +1486,8 @@ class StorageExplorerStore {
|
||||
}
|
||||
}
|
||||
|
||||
const subfolders = folderContents?.filter((item) => isNull(item.id)) ?? []
|
||||
const folderItems = folderContents?.filter((item) => !isNull(item.id)) ?? []
|
||||
const subfolders = folderContents?.filter((item) => item.id === null) ?? []
|
||||
const folderItems = folderContents?.filter((item) => item.id !== null) ?? []
|
||||
|
||||
folderItems.forEach((item) => items.push({ ...item, prefix: formattedPathToFolder }))
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@supabase/supabase-js": "^2.1.1",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-query-devtools": "^4.16.1",
|
||||
"@zip.js/zip.js": "^2.6.61",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dayjs from 'dayjs'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -30,6 +29,8 @@ import LogsExplorerHeader from 'components/ui/Logs/LogsExplorerHeader'
|
||||
|
||||
export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
|
||||
const { ref, q, ite, its } = router.query
|
||||
const [editorId, setEditorId] = useState<string>(uuidv4())
|
||||
const [editorValue, setEditorValue] = useState<string>('')
|
||||
@@ -221,12 +222,19 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
if (error) throw error
|
||||
setSubmitting(false)
|
||||
setSaveModalOpen(false)
|
||||
toast.success(`Saved "${values.name}" log query`)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Saved "${values.name}" log query`,
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
setSubmitting(false)
|
||||
setSaveModalOpen(false)
|
||||
toast.error(error.message)
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to save query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ReportsLayout from 'components/layouts'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { hooksFactory, usePresetReport } from 'components/interfaces/Reports/Reports.utils'
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
renderRequestsPathsTable,
|
||||
renderStatusCodesChart,
|
||||
renderBotScores,
|
||||
renderUserAgents
|
||||
renderUserAgents,
|
||||
} from 'components/interfaces/Reports/renderers/ApiRenderers'
|
||||
|
||||
export const ApiReport: NextPageWithLayout = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ReportsLayout from 'components/layouts'
|
||||
import { hooksFactory, usePresetReport } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
|
||||
Reference in new issue
Block a user