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:
Joshen Lim authored and GitHub committed 2023-01-09 21:54:49 +08:00
commit 70d08d973c
14 files changed
+377 -230

No files matched your search

+16
View File
@@ -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 (
+2
View File
@@ -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,
}
@@ -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
@@ -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>
)
@@ -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 }))
+1
View File
@@ -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}`,
})
}
}}
>
+2 -2
View File
@@ -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 = () => {
+1 -1
View File
@@ -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'