diff --git a/package-lock.json b/package-lock.json index 2125327d472..51dde1c8beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/studio/components/layouts/ReportsLayout/ReportsLayout.tsx b/studio/components/layouts/ReportsLayout/ReportsLayout.tsx index 3de10ebade9..6baddb56a35 100644 --- a/studio/components/layouts/ReportsLayout/ReportsLayout.tsx +++ b/studio/components/layouts/ReportsLayout/ReportsLayout.tsx @@ -12,9 +12,9 @@ interface ReportsLayoutProps { const ReportsLayout = ({ title, children }: PropsWithChildren) => { const { ui } = useStore() - const project = ui.selectedProject - const router = useRouter() + + const project = ui.selectedProject const page = router.pathname.split('/')[4] || '' return ( diff --git a/studio/components/layouts/index.ts b/studio/components/layouts/index.ts index 622cae0180b..7c64f282032 100644 --- a/studio/components/layouts/index.ts +++ b/studio/components/layouts/index.ts @@ -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, } diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx similarity index 83% rename from studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js rename to studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index ad57ec629c1..e4ab9f3e40e 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -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 (
{ ) } -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 }) => {
) : (
- {formattedBreadcrumbs.map((crumb, idx) => ( + {formattedBreadcrumbs.map((crumb: any, idx: number) => (
{idx !== 0 && }

{ ) } -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 = ({ 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 = ({ + Columns List , @@ -295,14 +332,14 @@ const FileExplorerHeader = ({ } type="text" disabled={breadcrumbs.length === 0} - onChange={onChangeView} + onChange={setView} > View as + Name Time created Time modified @@ -326,7 +363,7 @@ const FileExplorerHeader = ({ Ascending Descending @@ -352,6 +389,7 @@ const FileExplorerHeader = ({

+ {/* @ts-ignore */}
@@ -381,7 +419,7 @@ const FileExplorerHeader = ({ } actions={[ {}, - onSelectItemsDelete = () => {}, - onSelectItemsMove = () => {}, - onUnselectAllItems = () => {}, -}) => { +const FileExplorerHeaderSelection = () => { + const storageExplorerStore = useStorageStore() + const { + selectedItems, + downloadFile, + downloadSelectedFiles, + clearSelectedItems, + setSelectedItemsToDelete, + setSelectedItemsToMove, + } = storageExplorerStore + return (
@@ -34,7 +44,7 @@ const FileExplorerHeaderSelection = ({ icon={} type="primary" shadow={false} - onClick={onSelectItemsDelete} + onClick={() => setSelectedItemsToDelete(selectedItems)} > Delete @@ -42,7 +52,7 @@ const FileExplorerHeaderSelection = ({ icon={} type="primary" shadow={false} - onClick={onSelectItemsMove} + onClick={() => setSelectedItemsToMove(selectedItems)} > Move diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.js index efc6aa48d98..6712cc90e09 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.js @@ -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: , + onClick: () => onSelectItemRename(itemWithColumnIndex), + }, + { + name: 'Download', + icon: , + onClick: () => downloadFolder(itemWithColumnIndex), + }, + { name: 'Separator' }, + { + name: 'Delete', + icon: , + onClick: () => onSelectItemDelete(itemWithColumnIndex), + }, ] : [ ...(!item.isCorrupted diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js index 01d6629c32c..72d6ae9951b 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js @@ -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 ( onRenameFolder(props.item)}>Rename + downloadFolder(props.item)}>Download onDeleteFolder(props.item)}>Delete ) diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx similarity index 90% rename from studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.js rename to studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index 38b05a2c691..49066467eae 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -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 ( code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"') } @@ -36,7 +37,6 @@ const PreviewFile = ({ mimeType, previewUrl }) => {
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 ( 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()} />
@@ -182,7 +184,7 @@ const PreviewPane = ({ diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js index 831b5cfc906..23da2ae932e 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js @@ -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 (
{ > {selectedItems.length === 0 ? ( 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} /> ) : ( - + )}
{ fetchMoreFolderContents(index, column, itemSearchString) } /> - +
0} diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index c51f28e87f1..5e11d3c234c 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -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 })) diff --git a/studio/package.json b/studio/package.json index e35239fa471..b579303f7a3 100644 --- a/studio/package.json +++ b/studio/package.json @@ -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", diff --git a/studio/pages/project/[ref]/logs/explorer/index.tsx b/studio/pages/project/[ref]/logs/explorer/index.tsx index f166123a6f8..52d7906b166 100644 --- a/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -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(uuidv4()) const [editorValue, setEditorValue] = useState('') @@ -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}`, + }) } }} > diff --git a/studio/pages/project/[ref]/reports/api.tsx b/studio/pages/project/[ref]/reports/api.tsx index 1c6257d180b..48707940d5c 100644 --- a/studio/pages/project/[ref]/reports/api.tsx +++ b/studio/pages/project/[ref]/reports/api.tsx @@ -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 = () => { diff --git a/studio/pages/project/[ref]/reports/auth.tsx b/studio/pages/project/[ref]/reports/auth.tsx index d57839ebf23..6a164a3e9f5 100644 --- a/studio/pages/project/[ref]/reports/auth.tsx +++ b/studio/pages/project/[ref]/reports/auth.tsx @@ -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'