From fe9cb2743b6db210884519b434ad7f14498691c1 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 23 Apr 2024 13:16:55 +0200 Subject: [PATCH 1/6] Migrate all API calls to data folder. --- .../storage/bucket-object-delete-mutation.ts | 37 +++ .../bucket-object-download-mutation.ts | 37 +++ .../bucket-object-get-public-url-mutation.ts | 37 +++ .../storage/bucket-object-sign-mutation.ts | 38 +++ .../storage/bucket-objects-list-mutation.ts | 43 +++ .../data/storage/file-download-mutation.ts | 38 +++ .../data/storage/object-move-mutation.ts | 40 +++ .../storageExplorer/StorageExplorerStore.tsx | 247 ++++++++++-------- 8 files changed, 414 insertions(+), 103 deletions(-) create mode 100644 apps/studio/data/storage/bucket-object-delete-mutation.ts create mode 100644 apps/studio/data/storage/bucket-object-download-mutation.ts create mode 100644 apps/studio/data/storage/bucket-object-get-public-url-mutation.ts create mode 100644 apps/studio/data/storage/bucket-object-sign-mutation.ts create mode 100644 apps/studio/data/storage/bucket-objects-list-mutation.ts create mode 100644 apps/studio/data/storage/file-download-mutation.ts create mode 100644 apps/studio/data/storage/object-move-mutation.ts diff --git a/apps/studio/data/storage/bucket-object-delete-mutation.ts b/apps/studio/data/storage/bucket-object-delete-mutation.ts new file mode 100644 index 0000000000000..0484968eeae46 --- /dev/null +++ b/apps/studio/data/storage/bucket-object-delete-mutation.ts @@ -0,0 +1,37 @@ +import { useMutation } from '@tanstack/react-query' + +import { del, handleError } from 'data/fetchers' + +type DownloadBucketObjectParams = { + projectRef: string + bucketId?: string + paths: string[] +} +export const deleteBucketObject = async ( + { projectRef, bucketId, paths }: DownloadBucketObjectParams, + signal?: AbortSignal +) => { + if (!bucketId) throw new Error('bucketId is required') + + const { data, error } = await del('/platform/storage/{ref}/buckets/{id}/objects', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { + paths, + }, + signal, + }) + + if (error) handleError(error) + return data +} + +export function useBucketObjectDeleteMutation() { + return useMutation({ + mutationFn: deleteBucketObject, + }) +} diff --git a/apps/studio/data/storage/bucket-object-download-mutation.ts b/apps/studio/data/storage/bucket-object-download-mutation.ts new file mode 100644 index 0000000000000..7b8d004fd1634 --- /dev/null +++ b/apps/studio/data/storage/bucket-object-download-mutation.ts @@ -0,0 +1,37 @@ +import { useMutation } from '@tanstack/react-query' + +import { components } from 'data/api' +import { post } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' + +type DownloadBucketObjectParams = { + projectRef: string + bucketId?: string + path: string + options?: components['schemas']['DownloadObjectOptions'] +} +export const downloadBucketObject = async ( + { projectRef, bucketId, path, options }: DownloadBucketObjectParams, + signal?: AbortSignal +) => { + if (!bucketId) throw new Error('bucketId is required') + + // has to use lib/common/fetch post because the other post doesn't support wrapping blobs + const response = await post( + `${API_URL}/storage/${projectRef}/buckets/${bucketId}/objects/download`, + { + path, + options, + abortSignal: signal, + } + ) + + if (response.error) throw response.error + return response +} + +export function useBucketObjectDownloadMutation() { + return useMutation({ + mutationFn: downloadBucketObject, + }) +} diff --git a/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts new file mode 100644 index 0000000000000..f0dfe13f4e5b9 --- /dev/null +++ b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts @@ -0,0 +1,37 @@ +import { useMutation } from '@tanstack/react-query' + +import { components } from 'data/api' +import { handleError, post } from 'data/fetchers' + +type getPublicUrlForBucketObjectParams = { + projectRef: string + bucketId?: string + path: string + options?: components['schemas']['PublicUrlOptions'] +} +export const getPublicUrlForBucketObject = async ( + { projectRef, bucketId, path, options }: getPublicUrlForBucketObjectParams, + signal?: AbortSignal +) => { + if (!bucketId) throw new Error('bucketId is required') + + const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/public-url', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { path, options }, + signal, + }) + + if (error) handleError(error) + return data +} + +export function useBucketObjectGetPublicUrlMutation() { + return useMutation({ + mutationFn: getPublicUrlForBucketObject, + }) +} diff --git a/apps/studio/data/storage/bucket-object-sign-mutation.ts b/apps/studio/data/storage/bucket-object-sign-mutation.ts new file mode 100644 index 0000000000000..cf1fb0b2f4146 --- /dev/null +++ b/apps/studio/data/storage/bucket-object-sign-mutation.ts @@ -0,0 +1,38 @@ +import { useMutation } from '@tanstack/react-query' + +import { components } from 'data/api' +import { handleError, post } from 'data/fetchers' + +type signBucketObjectParams = { + projectRef: string + bucketId?: string + path: string + expiresIn: number + options?: components['schemas']['SignedUrlOptions'] +} +export const signBucketObject = async ( + { projectRef, bucketId, path, expiresIn, options }: signBucketObjectParams, + signal?: AbortSignal +) => { + if (!bucketId) throw new Error('bucketId is required') + + const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/sign', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { path, expiresIn, options }, + signal, + }) + + if (error) handleError(error) + return data +} + +export function useSignBucketObjectMutation() { + return useMutation({ + mutationFn: signBucketObject, + }) +} diff --git a/apps/studio/data/storage/bucket-objects-list-mutation.ts b/apps/studio/data/storage/bucket-objects-list-mutation.ts new file mode 100644 index 0000000000000..65ff8ed36974e --- /dev/null +++ b/apps/studio/data/storage/bucket-objects-list-mutation.ts @@ -0,0 +1,43 @@ +import { useMutation } from '@tanstack/react-query' + +import { components } from 'data/api' +import { handleError, post } from 'data/fetchers' + +type ListBucketObjectsParams = { + projectRef: string + bucketId?: string + path: string + options: components['schemas']['StorageObjectSearchOptions'] +} + +export type StorageObject = components['schemas']['StorageObject'] + +export const listBucketObjects = async ( + { projectRef, bucketId, path, options }: ListBucketObjectsParams, + signal?: AbortSignal +) => { + if (!bucketId) throw new Error('bucketId is required') + + const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/list', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { + path, + options, + }, + signal, + }) + + if (error) handleError(error) + return data +} + +export function useBucketObjectsListMutation() { + return useMutation({ + mutationFn: listBucketObjects, + }) +} diff --git a/apps/studio/data/storage/file-download-mutation.ts b/apps/studio/data/storage/file-download-mutation.ts new file mode 100644 index 0000000000000..2fa0541b540bc --- /dev/null +++ b/apps/studio/data/storage/file-download-mutation.ts @@ -0,0 +1,38 @@ +import { useMutation } from '@tanstack/react-query' + +import { components } from 'data/api' +import { post } from 'data/fetchers' + +type ListBucketObjectsParams = { + projectRef: string + bucketId: string + path: string + options: components['schemas']['StorageObjectSearchOptions'] +} +const listBucketObjects = async ({ + projectRef, + bucketId, + path, + options, +}: ListBucketObjectsParams) => { + const res = await post('/platform/storage/{ref}/buckets/{id}/objects/list', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { + path, + options, + }, + }) + + return res +} + +export function useBucketObjectsListMutation() { + return useMutation({ + mutationFn: listBucketObjects, + }) +} diff --git a/apps/studio/data/storage/object-move-mutation.ts b/apps/studio/data/storage/object-move-mutation.ts new file mode 100644 index 0000000000000..7a17ea1beddd5 --- /dev/null +++ b/apps/studio/data/storage/object-move-mutation.ts @@ -0,0 +1,40 @@ +import { useMutation } from '@tanstack/react-query' + +import { handleError, post } from 'data/fetchers' + +type MoveStorageObjectParams = { + projectRef: string + bucketId?: string + from: string + to: string +} +export const moveStorageObject = async ({ + projectRef, + bucketId, + from, + to, +}: MoveStorageObjectParams) => { + if (!bucketId) throw new Error('bucketId is required') + + const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/move', { + params: { + path: { + ref: projectRef, + id: bucketId, + }, + }, + body: { + from, + to, + }, + }) + + if (error) handleError(error) + return data +} + +export function useBucketObjectsMoveMutation() { + return useMutation({ + mutationFn: moveStorageObject, + }) +} diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index 1c099791c4e58..c54e9ab7de7b2 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -17,7 +17,12 @@ import { ToastLoader } from 'components/ui/ToastLoader' import { configKeys } from 'data/config/keys' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' import { getQueryClient } from 'data/query-client' -import { delete_, post } from 'lib/common/fetch' +import { deleteBucketObject } from 'data/storage/bucket-object-delete-mutation' +import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation' +import { getPublicUrlForBucketObject } from 'data/storage/bucket-object-get-public-url-mutation' +import { signBucketObject } from 'data/storage/bucket-object-sign-mutation' +import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation' +import { moveStorageObject } from 'data/storage/object-move-mutation' import { API_URL, IS_PLATFORM } from 'lib/constants' import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants' @@ -47,7 +52,7 @@ class StorageExplorerStore { sortBy = STORAGE_SORT_BY.NAME sortByOrder = 'asc' buckets = [] - selectedBucket = {} + selectedBucket: { id?: string } = {} columns = [] openedFolders = [] selectedItems = [] @@ -74,7 +79,7 @@ class StorageExplorerStore { uploadProgress = 0 /* Controllers to abort API calls */ - abortController = null + abortController: AbortController | null = null constructor(projectRef) { makeAutoObservable(this, { supabaseClient: false }) @@ -308,7 +313,9 @@ class StorageExplorerStore { .upload(formattedPathToEmptyPlaceholderFile, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) if (pathToFolder.length > 0) { - await delete_(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects`, { + await deleteBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, paths: [`${pathToFolder}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`], }) } @@ -629,7 +636,9 @@ class StorageExplorerStore { }, Promise.resolve()) if (numberOfFilesUploadedSuccess > 0) { - await delete_(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects`, { + await deleteBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, paths: [`${pathToFile}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`], }) } @@ -690,13 +699,16 @@ class StorageExplorerStore { const toPath = newPathToFile.length > 0 ? `${formattedNewPathToFile}/${item.name}` : item.name - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/move`, { - from: fromPath, - to: toPath, - }) - if (res.error) { + try { + await moveStorageObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + from: fromPath, + to: toPath, + }) + } catch (error: any) { numberOfFilesMovedFail += 1 - toast.error(res.error.message) + toast.error(error.message) } }) ) @@ -730,24 +742,27 @@ class StorageExplorerStore { const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName if (this.selectedBucket.public) { - const res = await post( - `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/public-url`, - { path: formattedPathToFile } - ) - if (!res.error) { - return res.publicUrl - } else { - toast.error(`Failed to fetch public file preview: ${res.error.message}`) + try { + const data = await getPublicUrlForBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: formattedPathToFile, + }) + return data.publicUrl + } catch (error: any) { + toast.error(`Failed to fetch public file preview: ${error.message}`) } } else { - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/sign`, { - path: formattedPathToFile, - expiresIn: expiresIn || DEFAULT_EXPIRY, - }) - if (!res.error) { - return res.signedUrl - } else { - toast.error(`Failed to fetch signed url preview: ${res.error.message}`) + try { + const data = await signBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: formattedPathToFile, + expiresIn: expiresIn || DEFAULT_EXPIRY, + }) + return data.signedUrl + } catch (error: any) { + toast.error(`Failed to fetch signed url preview: ${error.message}`) } } return null @@ -784,8 +799,10 @@ class StorageExplorerStore { // batch BATCH_SIZE prefixes per request const batches = chunk(prefixes, BATCH_SIZE).map((batch) => () => { progress = progress + batch.length / prefixes.length - return delete_(`${this.endpoint}/buckets/${this.selectedBucket.name}/objects`, { - paths: batch, + return deleteBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + paths: batch as string[], }) }) @@ -848,20 +865,21 @@ class StorageExplorerStore { const fileMimeType = file.metadata?.mimetype ?? null return () => { return new Promise(async (resolve) => { - const res = await post( - `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/download`, - { path: `${file.prefix}/${file.name}` } - ) - progress = progress + 1 / files.length + try { + const data = await downloadBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: `${file.prefix}/${file.name}`, + }) + progress = progress + 1 / files.length - if (!res.error) { - const blob = await res.blob() + const blob = await data.blob() resolve({ name: file.name, prefix: file.prefix, blob: new Blob([blob], { type: fileMimeType }), }) - } else { + } catch (error) { console.error('Failed to download file', `${file.prefix}/${file.name}`) resolve(false) } @@ -986,12 +1004,14 @@ class StorageExplorerStore { .map((folder) => folder.name) .join('/') const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/download`, { - path: formattedPathToFile, - }) + try { + const data = await downloadBucketObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: formattedPathToFile, + }) - if (!res.error) { - const blob = await res.blob() + const blob = await data.blob() const newBlob = new Blob([blob], { type: fileMimeType }) if (returnBlob) return { name: fileName, blob: newBlob } @@ -1008,7 +1028,7 @@ class StorageExplorerStore { toast.success(`Downloading ${fileName}`, { id: toastId }) } return true - } else { + } catch { if (toastId) { toast.error(`Failed to download ${fileName}`, { id: toastId }) } @@ -1028,14 +1048,14 @@ class StorageExplorerStore { const fromPath = pathToFile.length > 0 ? `${pathToFile}/${originalName}` : originalName const toPath = pathToFile.length > 0 ? `${pathToFile}/${newName}` : newName - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/move`, { - from: fromPath, - to: toPath, - }) + try { + const data = await moveStorageObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + from: fromPath, + to: toPath, + }) - if (res.error) { - toast.error(`Failed to rename file: ${res.error.message}`) - } else { toast.success(`Successfully renamed "${originalName}" to "${newName}"`) // Clear file preview cache if the renamed file exists in the cache @@ -1050,6 +1070,8 @@ class StorageExplorerStore { } await this.refetchAllOpenedFolders() + } catch (error) { + toast.error(`Failed to rename file: ${error.message}`) } } } @@ -1077,16 +1099,20 @@ class StorageExplorerStore { sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const res = await post( - `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, - { path: prefix, options }, - { abortSignal: this.abortController.signal } - ) + try { + const data = await listBucketObjects( + { + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: prefix, + options, + }, + this.abortController?.signal + ) - this.updateRowStatus(folderName, STORAGE_ROW_STATUS.READY, index) + this.updateRowStatus(folderName, STORAGE_ROW_STATUS.READY, index) - if (!res.error) { - const formattedItems = this.formatFolderItems(res) + const formattedItems = this.formatFolderItems(data) this.pushColumnAtIndex( { id: folderId || folderName, @@ -1097,8 +1123,10 @@ class StorageExplorerStore { }, index ) - } else if (!res.error.message.includes('aborted')) { - toast.error(`Failed to retrieve folder contents from "${folderName}": ${res.error.message}`) + } catch (error: any) { + if (!error.message.includes('aborted')) { + toast.error(`Failed to retrieve folder contents from "${folderName}": ${error.message}`) + } } } @@ -1113,28 +1141,29 @@ class StorageExplorerStore { sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const res = await post( - `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, - { path: prefix, options }, - { abortSignal: this.abortController.signal } - ) + try { + const data = await listBucketObjects( + { projectRef: this.projectRef, bucketId: this.selectedBucket.id, path: prefix, options }, + this.abortController?.signal + ) - if (!res.error) { // Add items to column - const formattedItems = this.formatFolderItems(res) + const formattedItems = this.formatFolderItems(data) this.columns = this.columns.map((col, idx) => { if (idx === index) { return { ...col, items: col.items.concat(formattedItems), isLoadingMoreItems: false, - hasMoreItems: res.length === LIMIT, + hasMoreItems: data.length === LIMIT, } } return col }) - } else if (!res.error.message.includes('aborted')) { - toast.error(`Failed to retrieve folder contents from "${folderName}": ${res.error.message}`) + } catch (error: any) { + if (!error.message.includes('aborted')) { + toast.error(`Failed to retrieve folder contents from "${folderName}": ${error.message}`) + } } } @@ -1164,15 +1193,18 @@ class StorageExplorerStore { sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, { - path: prefix, - options, - }) - if (res.error) { - toast.error(`Failed to fetch folders: ${res.error.message}`) + try { + const data = await listBucketObjects({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: prefix, + options, + }) + return data + } catch (error: any) { + toast.error(`Failed to fetch folders: ${error.message}`) return [] } - return res }) ) @@ -1210,16 +1242,21 @@ class StorageExplorerStore { // Check parent folder if its empty, if yes, reinstate .emptyFolderPlaceholder // Used when deleting folder or deleting files validateParentFolderEmpty = async (parentFolderPrefix) => { - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, { - path: parentFolderPrefix, - options: this.DEFAULT_OPTIONS, - }) - if (!res.error && res.length === 0) { - const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` - await this.supabaseClient.storage - .from(this.selectedBucket.name) - .upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) - } + try { + const data = await listBucketObjects({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: parentFolderPrefix, + options: this.DEFAULT_OPTIONS, + }) + + if (data.length === 0) { + const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` + await this.supabaseClient.storage + .from(this.selectedBucket.name) + .upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) + } + } catch (error) {} } deleteFolder = async (folder) => { @@ -1288,14 +1325,14 @@ class StorageExplorerStore { return () => { return new Promise(async (resolve) => { progress = progress + 1 / files.length - const res = await post( - `${this.endpoint}/buckets/${this.selectedBucket.name}/objects/move`, - { + try { + await moveStorageObject({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, from: fromPath, to: toPath, - } - ) - if (res.error) { + }) + } catch (error) { hasErrors = true toast.error(`Failed to move ${fromPath} to the new folder`) } @@ -1372,15 +1409,19 @@ class StorageExplorerStore { let folderContents = [] for (;;) { - const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.name}/objects/list`, { - path: formattedPathToFolder, - options, - }) - folderContents = folderContents.concat(res) - options.offset += options.limit - if ((res || []).length < options.limit) { - break - } + try { + const data = await listBucketObjects({ + projectRef: this.projectRef, + bucketId: this.selectedBucket.id, + path: formattedPathToFolder, + options, + }) + folderContents = folderContents.concat(data) + options.offset += options.limit + if ((data || []).length < options.limit) { + break + } + } catch (e) {} } const subfolders = folderContents?.filter((item) => item.id === null) ?? [] @@ -1436,7 +1477,7 @@ class StorageExplorerStore { return name } - formatFolderItems = (items = []) => { + formatFolderItems = (items: StorageObject[] = []) => { const formattedItems = (items ?? []) ?.filter((item) => item.name !== EMPTY_FOLDER_PLACEHOLDER_FILE_NAME) From fa3c1f491fda85db1f2eabd39a519b1a2c7cf520 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 25 Apr 2024 14:43:51 +0200 Subject: [PATCH 2/6] Remove unused mutation. --- .../data/storage/file-download-mutation.ts | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 apps/studio/data/storage/file-download-mutation.ts diff --git a/apps/studio/data/storage/file-download-mutation.ts b/apps/studio/data/storage/file-download-mutation.ts deleted file mode 100644 index 2fa0541b540bc..0000000000000 --- a/apps/studio/data/storage/file-download-mutation.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMutation } from '@tanstack/react-query' - -import { components } from 'data/api' -import { post } from 'data/fetchers' - -type ListBucketObjectsParams = { - projectRef: string - bucketId: string - path: string - options: components['schemas']['StorageObjectSearchOptions'] -} -const listBucketObjects = async ({ - projectRef, - bucketId, - path, - options, -}: ListBucketObjectsParams) => { - const res = await post('/platform/storage/{ref}/buckets/{id}/objects/list', { - params: { - path: { - ref: projectRef, - id: bucketId, - }, - }, - body: { - path, - options, - }, - }) - - return res -} - -export function useBucketObjectsListMutation() { - return useMutation({ - mutationFn: listBucketObjects, - }) -} From 061b65333a813d51b792facc7dfeb596101ec73c Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 25 Apr 2024 14:44:07 +0200 Subject: [PATCH 3/6] MInor leftover type fixes. --- .../localStores/storageExplorer/StorageExplorerStore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index c54e9ab7de7b2..8a0eeb0108e27 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -1070,7 +1070,7 @@ class StorageExplorerStore { } await this.refetchAllOpenedFolders() - } catch (error) { + } catch (error: any) { toast.error(`Failed to rename file: ${error.message}`) } } @@ -1162,7 +1162,7 @@ class StorageExplorerStore { }) } catch (error: any) { if (!error.message.includes('aborted')) { - toast.error(`Failed to retrieve folder contents from "${folderName}": ${error.message}`) + toast.error(`Failed to retrieve folder contents from "${prefix}": ${error.message}`) } } } From 0abcea698b60a4696921216ab9d237cce1147102 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 6 Jun 2024 10:39:07 +0200 Subject: [PATCH 4/6] chore: Make the `StorageExplorerStore` fully typed (#23397) * Migrate all API calls to data folder. * Convert the poor man enums into proper enums. * Add types to most of the methods. * Add types around the methods for uploading files. * Add types for file cache. * Add types for buckets. * Fix bunch of type errors. * Add types to all store methods. Fix all types in components related to the store. * Remove all anys from the Storage components. * Fix the multiple selection. * Fix the selected checkbox. * Set all private methods to private and delete some unneeded code. * Remove unneeded mutation. --- .../layouts/StorageLayout/StorageLayout.tsx | 3 +- .../Storage/Storage.constants.ts | 38 +- .../to-be-cleaned/Storage/Storage.types.ts | 14 +- .../StorageExplorer/ColumnContextMenu.tsx | 12 +- .../StorageExplorer/ConfirmDeleteModal.tsx | 3 +- .../StorageExplorer/CustomExpiryModal.tsx | 4 +- .../Storage/StorageExplorer/FileExplorer.tsx | 22 +- .../StorageExplorer/FileExplorerColumn.tsx | 14 +- .../StorageExplorer/FileExplorerHeader.tsx | 18 +- .../StorageExplorer/FileExplorerRow.tsx | 43 +- .../FileExplorerRowEditing.tsx | 11 +- .../StorageExplorer/ItemContextMenu.tsx | 3 +- .../StorageExplorer/MoveItemsModal.tsx | 3 +- .../Storage/StorageExplorer/PreviewPane.tsx | 11 +- .../StorageExplorer/StorageExplorer.tsx | 16 +- .../StorageExplorer/StorageExplorer.utils.ts | 5 +- .../storageExplorer/StorageExplorerStore.tsx | 459 ++++++++++-------- 17 files changed, 392 insertions(+), 287 deletions(-) diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index 3d77e112835c7..4e66b228a7c7f 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -33,7 +33,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { if (apiService.endpoint) { storageExplorerStore.initStore( - projectRef, + projectRef!, apiService.endpoint, apiService.serviceApiKey, apiService.protocol @@ -43,7 +43,6 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { 'Failed to fetch project configuration. Try refreshing your browser, or reach out to us at support@supabase.io' ) } - storageExplorerStore.setLoaded(true) } return ( diff --git a/apps/studio/components/to-be-cleaned/Storage/Storage.constants.ts b/apps/studio/components/to-be-cleaned/Storage/Storage.constants.ts index e3a858d7a4730..1eef8024d1559 100644 --- a/apps/studio/components/to-be-cleaned/Storage/Storage.constants.ts +++ b/apps/studio/components/to-be-cleaned/Storage/Storage.constants.ts @@ -4,33 +4,33 @@ export const URL_EXPIRY_DURATION = { YEAR: 60 * 60 * 24 * 365, } -export const STORAGE_VIEWS = { - COLUMNS: 'COLUMNS', - LIST: 'LIST', +export enum STORAGE_VIEWS { + COLUMNS = 'COLUMNS', + LIST = 'LIST', } -export const STORAGE_SORT_BY = { - NAME: 'name', - UPDATED_AT: 'updated_at', - CREATED_AT: 'created_at', - LAST_ACCESSED_AT: 'last_accessed_at', +export enum STORAGE_SORT_BY { + NAME = 'name', + UPDATED_AT = 'updated_at', + CREATED_AT = 'created_at', + LAST_ACCESSED_AT = 'last_accessed_at', } -export const STORAGE_SORT_BY_ORDER = { - ASC: 'asc', - DESC: 'desc', +export enum STORAGE_SORT_BY_ORDER { + ASC = 'asc', + DESC = 'desc', } -export const STORAGE_ROW_TYPES = { - BUCKET: 'BUCKET', - FILE: 'FILE', - FOLDER: 'FOLDER', +export enum STORAGE_ROW_TYPES { + BUCKET = 'BUCKET', + FILE = 'FILE', + FOLDER = 'FOLDER', } -export const STORAGE_ROW_STATUS = { - READY: 'READY', - LOADING: 'LOADING', - EDITING: 'EDITING', +export enum STORAGE_ROW_STATUS { + READY = 'READY', + LOADING = 'LOADING', + EDITING = 'EDITING', } export const STORAGE_CLIENT_LIBRARY_MAPPINGS = { diff --git a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts index fcb2ac2e40598..e52f7011b5708 100644 --- a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts +++ b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts @@ -1,17 +1,19 @@ +import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES } from './Storage.constants' + export interface StorageColumn { - id: string + id: string | null name: string status: string items: StorageItem[] - hasMoreItems: boolean - isLoadingMoreItems: boolean + hasMoreItems?: boolean + isLoadingMoreItems?: boolean } export interface StorageItem { id: string | null name: string - type: string - status: string + type: STORAGE_ROW_TYPES + status: STORAGE_ROW_STATUS metadata: StorageItemMetadata | null isCorrupted: boolean created_at: string | null @@ -19,6 +21,8 @@ export interface StorageItem { last_accessed_at: string | null } +export type StorageItemWithColumn = StorageItem & { columnIndex: number } + export interface StorageItemMetadata { cacheControl: string contentLength: number diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx index a8dd499860267..864733a29d941 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -40,19 +40,19 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { const onSelectAllItemsInColumn = (columnIndex: number) => { const columnFiles = columns[columnIndex].items - .filter((item: any) => item.type === STORAGE_ROW_TYPES.FILE) - .map((item: any) => { + .filter((item) => item.type === STORAGE_ROW_TYPES.FILE) + .map((item) => { return { ...item, columnIndex } }) - const columnFilesId = compact(columnFiles.map((item: any) => item.id)) - const selectedItemsFromColumn = selectedItems.filter((item: any) => - columnFilesId.includes(item.id) + const columnFilesId = compact(columnFiles.map((item) => item.id)) + const selectedItemsFromColumn = selectedItems.filter( + (item) => item.id && columnFilesId.includes(item.id) ) if (selectedItemsFromColumn.length === columnFiles.length) { // Deselect all items from column const updatedSelectedItems = selectedItems.filter( - (item: any) => !columnFilesId.includes(item.id) + (item) => item.id && !columnFilesId.includes(item.id) ) setSelectedItems(updatedSelectedItems) } else { diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx index c782c431f276d..cf1a537573f4f 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx @@ -1,10 +1,11 @@ import { noop } from 'lodash' import { useEffect, useState } from 'react' import { Alert, Button, Modal } from 'ui' +import { StorageItem } from '../Storage.types' interface ConfirmDeleteModalProps { visible: boolean - selectedItemsToDelete: any[] + selectedItemsToDelete: StorageItem[] onSelectCancel: () => void onSelectDelete: () => void } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/CustomExpiryModal.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/CustomExpiryModal.tsx index 323f90fef722d..6614a57ee98e9 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/CustomExpiryModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/CustomExpiryModal.tsx @@ -39,9 +39,9 @@ const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => { onSubmit={async (values: any, { setSubmitting }: any) => { setSubmitting(true) onCopyUrl( - selectedFileCustomExpiry.name, + selectedFileCustomExpiry!.name, await getFileUrl( - selectedFileCustomExpiry, + selectedFileCustomExpiry!, values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years'] ) ) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx index 9392c7e5db9c4..1993b1695f195 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx @@ -1,20 +1,20 @@ -import { useEffect, useRef } from 'react' import { observer } from 'mobx-react-lite' +import { useEffect, useRef } from 'react' -import { STORAGE_VIEWS, CONTEXT_MENU_KEYS } from '../Storage.constants' -import ItemContextMenu from './ItemContextMenu' -import FolderContextMenu from './FolderContextMenu' +import { noop } from 'lodash' +import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' +import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' import ColumnContextMenu from './ColumnContextMenu' import FileExplorerColumn from './FileExplorerColumn' -import { noop } from 'lodash' -import type { StorageColumn } from '../Storage.types' +import FolderContextMenu from './FolderContextMenu' +import ItemContextMenu from './ItemContextMenu' export interface FileExplorerProps { view: string - columns: any[] - openedFolders: any[] - selectedItems: any[] - selectedFilePreview: any + columns: StorageColumn[] + openedFolders: StorageItem[] + selectedItems: StorageItemWithColumn[] + selectedFilePreview: (StorageItemWithColumn & { previewUrl: string | undefined }) | null itemSearchString: string onFilesUpload: (event: any, index: number) => void onSelectAllItemsInColumn: (index: number) => void @@ -28,7 +28,7 @@ const FileExplorer = ({ columns = [], openedFolders = [], selectedItems = [], - selectedFilePreview = {}, + selectedFilePreview, itemSearchString, onFilesUpload = noop, onSelectAllItemsInColumn = noop, diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx index 8ac8f205a558e..bde5fd2b72a22 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -15,7 +15,7 @@ import { STORAGE_ROW_TYPES, STORAGE_VIEWS, } from '../Storage.constants' -import type { StorageColumn } from '../Storage.types' +import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRow from './FileExplorerRow' const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => { @@ -54,12 +54,12 @@ const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => export interface FileExplorerColumnProps { index: number - view: string + view: STORAGE_VIEWS column: StorageColumn fullWidth?: boolean - openedFolders?: any[] - selectedItems: any[] - selectedFilePreview: any + openedFolders?: StorageItem[] + selectedItems: StorageItemWithColumn[] + selectedFilePreview: (StorageItemWithColumn & { previewUrl: string | undefined }) | null itemSearchString: string onFilesUpload: (event: any, index: number) => void onSelectAllItemsInColumn: (index: number) => void @@ -75,7 +75,7 @@ const FileExplorerColumn = ({ fullWidth = false, openedFolders = [], selectedItems = [], - selectedFilePreview = {}, + selectedFilePreview, itemSearchString, onFilesUpload = noop, onSelectAllItemsInColumn = noop, @@ -105,7 +105,7 @@ const FileExplorerColumn = ({ (item) => item.type === STORAGE_ROW_TYPES.FILE ) - const columnItems = column.items + const columnItems = column.items.map((item, index) => ({ ...item, columnIndex: index })) const columnItemsSize = sum(columnItems.map((item) => get(item, ['metadata', 'size'], 0))) const isEmpty = diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index d1b725d7477c6..e34509641a122 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -78,7 +78,17 @@ const HeaderPathEdit = ({ loading, isSearching, breadcrumbs, togglePathEdit }: a ) } -const HeaderBreadcrumbs = ({ loading, isSearching, breadcrumbs, selectBreadcrumb }: any) => { +const HeaderBreadcrumbs = ({ + loading, + isSearching, + breadcrumbs, + selectBreadcrumb, +}: { + loading: { isLoading: boolean; message: string } + isSearching: boolean + breadcrumbs: string[] + selectBreadcrumb: (i: number) => void +}) => { // Max 5 crumbs, otherwise replace middle segment with ellipsis and only // have the first 2 and last 2 crumbs visible const ellipsis = '...' @@ -106,7 +116,7 @@ const HeaderBreadcrumbs = ({ loading, isSearching, breadcrumbs, selectBreadcrumb ) : (
- {formattedBreadcrumbs.map((crumb: any, idx: number) => ( + {formattedBreadcrumbs.map((crumb, idx: number) => (
{idx !== 0 && }

column.name) + const breadcrumbs = columns.map((column) => column.name) const backDisabled = columns.length <= 1 const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') @@ -225,7 +235,7 @@ const FileExplorerHeader = ({ onSetPathByString(compact(pathString.split('/'))) } - const onSetPathByString = async (paths: any[]) => { + const onSetPathByString = async (paths: string[]) => { if (paths.length === 0) { popColumnAtIndex(0) clearOpenedFolders() diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx index 6ece67a3f983f..0e8d7c4c53698 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx @@ -40,10 +40,21 @@ import { STORAGE_VIEWS, URL_EXPIRY_DURATION, } from '../Storage.constants' +import { StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRowEditing from './FileExplorerRowEditing' import { copyPathToFolder } from './StorageExplorer.utils' -export const RowIcon = ({ view, status, fileType, mimeType }: any) => { +export const RowIcon = ({ + view, + status, + fileType, + mimeType, +}: { + view: STORAGE_VIEWS + status: STORAGE_ROW_STATUS + fileType: string + mimeType: string | undefined +}) => { if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) { return } @@ -81,22 +92,22 @@ export const RowIcon = ({ view, status, fileType, mimeType }: any) => { } export interface FileExplorerRowProps { - view: string + view: STORAGE_VIEWS columnIndex: number - selectedItems: any[] - openedFolders: any[] - selectedFilePreview: any + selectedItems: StorageItemWithColumn[] + openedFolders: StorageItem[] + selectedFilePreview: (StorageItemWithColumn & { previewUrl: string | undefined }) | null onCopyUrl: (name: string, url: string) => void } -const FileExplorerRow: ItemRenderer = ({ +const FileExplorerRow: ItemRenderer = ({ index: itemIndex, - item = {}, + item, view = STORAGE_VIEWS.COLUMNS, columnIndex = 0, selectedItems = [], openedFolders = [], - selectedFilePreview = {}, + selectedFilePreview, onCopyUrl, }) => { const storageExplorerStore = useStorageStore() @@ -122,22 +133,22 @@ const FileExplorerRow: ItemRenderer = ({ const isPublic = selectedBucket.public const itemWithColumnIndex = { ...item, columnIndex } - const isSelected = find(selectedItems, item) !== undefined + const isSelected = !!selectedItems.find((i) => i.id === item.id) const isOpened = openedFolders.length > columnIndex ? isEqual(openedFolders[columnIndex], item) : false - const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview.id, item.id) + const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id) const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') const { show } = useContextMenu() - const onSelectFile = async (columnIndex: number, file: any) => { + const onSelectFile = async (columnIndex: number, file: StorageItem) => { popColumnAtIndex(columnIndex) popOpenedFoldersAtIndex(columnIndex - 1) - setFilePreview(file) + setFilePreview(itemWithColumnIndex) clearSelectedItems() } - const onSelectFolder = async (columnIndex: number, folder: any) => { + const onSelectFolder = async (columnIndex: number, folder: StorageItem) => { closeFilePreview() clearSelectedItems(columnIndex + 1) popOpenedFoldersAtIndex(columnIndex - 1) @@ -151,9 +162,9 @@ const FileExplorerRow: ItemRenderer = ({ selectRangeItems(columnIndex, itemIndex) return } - if (find(selectedItems, (item: any) => itemWithColumnIndex.id === item.id) !== undefined) { + if (find(selectedItems, (item) => itemWithColumnIndex.id === item.id) !== undefined) { setSelectedItems( - selectedItems.filter((selectedItem: any) => itemWithColumnIndex.id !== selectedItem.id) + selectedItems.filter((selectedItem) => itemWithColumnIndex.id !== selectedItem.id) ) } else { setSelectedItems([...selectedItems, itemWithColumnIndex]) @@ -283,7 +294,7 @@ const FileExplorerRow: ItemRenderer = ({ const createdAt = item.created_at ? new Date(item.created_at).toLocaleString() : '-' const updatedAt = item.updated_at ? new Date(item.updated_at).toLocaleString() : '-' - const displayMenu = (event: any, rowType: any) => { + const displayMenu = (event: any, rowType: STORAGE_ROW_TYPES) => { show(event, { id: rowType === STORAGE_ROW_TYPES.FILE diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRowEditing.tsx index 176f0388d6ac7..d447371d726b2 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -1,12 +1,13 @@ -import { has } from 'lodash' -import { useState, useEffect, useRef } from 'react' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' -import { STORAGE_ROW_TYPES } from '../Storage.constants' +import { has } from 'lodash' +import { useEffect, useRef, useState } from 'react' +import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' +import { StorageItem } from '../Storage.types' import { RowIcon } from './FileExplorerRow' export interface FileExplorerRowEditingProps { - item: any - view: string + item: StorageItem + view: STORAGE_VIEWS columnIndex: number } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx index 07971cd72e4d5..9716d69e101d3 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx @@ -8,6 +8,7 @@ import { useCheckPermissions } from 'hooks' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { IconChevronRight, IconClipboard, IconDownload, IconEdit, IconMove, IconTrash2 } from 'ui' import { URL_EXPIRY_DURATION } from '../Storage.constants' +import { StorageItemWithColumn } from '../Storage.types' interface ItemContextMenuProps { id: string @@ -28,7 +29,7 @@ const ItemContextMenu = ({ id = '', onCopyUrl = noop }: ItemContextMenuProps) => const isPublic = selectedBucket.public const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') - const onHandleClick = async (event: any, item: any, expiresIn?: number) => { + const onHandleClick = async (event: any, item: StorageItemWithColumn, expiresIn?: number) => { if (item.isCorrupted) return switch (event) { case 'copy': diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx index ac3d2c8edf275..b76ddea329f91 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx @@ -1,11 +1,12 @@ import { noop } from 'lodash' import { useEffect, useState } from 'react' import { Button, Input, Modal } from 'ui' +import { StorageItemWithColumn } from '../Storage.types' interface MoveItemsModalProps { bucketName: string visible: boolean - selectedItemsToMove: any[] + selectedItemsToMove: StorageItemWithColumn[] onSelectCancel: () => void onSelectMove: (path: string) => void } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index 3e4ea73b71bbe..c1301672b5c2b 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -24,7 +24,7 @@ import { useCheckPermissions } from 'hooks' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { URL_EXPIRY_DURATION } from '../Storage.constants' -const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: string }) => { +const PreviewFile = ({ mimeType, previewUrl }: { mimeType?: string; previewUrl?: string }) => { if (!mimeType || !previewUrl) { return ( { setSelectedFileCustomExpiry, } = storageExplorerStore + const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + + if (!file) { + return <> + } + 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 mimeType = file.metadata ? file.metadata.mimetype : undefined const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown' const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown' - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') return ( <> diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index 95e4943af5082..bcccc9e071f99 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react' import toast from 'react-hot-toast' import { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import type { Bucket } from 'data/storage/buckets-query' import { DEFAULT_PROJECT_API_SERVICE_ID, IS_PLATFORM } from 'lib/constants' @@ -18,7 +19,6 @@ import FileExplorerHeader from './FileExplorerHeader' import FileExplorerHeaderSelection from './FileExplorerHeaderSelection' import MoveItemsModal from './MoveItemsModal' import PreviewPane from './PreviewPane' -import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' interface StorageExplorerProps { bucket: Bucket @@ -106,7 +106,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { } } } else if (view === STORAGE_VIEWS.COLUMNS) { - const paths = openedFolders.map((folder: any) => folder.name) + const paths = openedFolders.map((folder) => folder.name) fetchFoldersByPath(paths, itemSearchString, true) } } @@ -128,19 +128,19 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { const onSelectAllItemsInColumn = (columnIndex: number) => { const columnFiles = columns[columnIndex].items - .filter((item: any) => item.type === STORAGE_ROW_TYPES.FILE) - .map((item: any) => { + .filter((item) => item.type === STORAGE_ROW_TYPES.FILE) + .map((item) => { return { ...item, columnIndex } }) - const columnFilesId = compact(columnFiles.map((item: any) => item.id)) - const selectedItemsFromColumn = selectedItems.filter((item: any) => - columnFilesId.includes(item.id) + const columnFilesId = compact(columnFiles.map((item) => item.id)) + const selectedItemsFromColumn = selectedItems.filter( + (item) => item.id && columnFilesId.includes(item.id) ) if (selectedItemsFromColumn.length === columnFiles.length) { // Deselect all items from column const updatedSelectedItems = selectedItems.filter( - (item: any) => !columnFilesId.includes(item.id) + (item) => item.id && !columnFilesId.includes(item.id) ) setSelectedItems(updatedSelectedItems) } else { diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.ts b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.ts index 20fa17aba92de..e6e3af3eb8e9e 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.ts +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.ts @@ -1,8 +1,9 @@ import { copyToClipboard } from 'lib/helpers' import toast from 'react-hot-toast' +import { StorageItem, StorageItemWithColumn } from '../Storage.types' -export const copyPathToFolder = (openedFolders: any[], item: any) => { - const folders = openedFolders.slice(0, item.columnIndex).map((folder: any) => folder.name) +export const copyPathToFolder = (openedFolders: StorageItem[], item: StorageItemWithColumn) => { + const folders = openedFolders.slice(0, item.columnIndex).map((folder) => folder.name) const path = folders.length > 0 ? `${folders.join('/')}/${item.name}` : item.name copyToClipboard(path) toast.success(`Copied path to folder "${item.name}"`) diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index 8a0eeb0108e27..ce8c8ea067c48 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -1,7 +1,6 @@ -// @ts-nocheck -import { createClient } from '@supabase/supabase-js' +import { SupabaseClient, createClient } from '@supabase/supabase-js' import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' -import { chunk, compact, find, findIndex, has, isEqual, some, uniq, uniqBy } from 'lodash' +import { chunk, compact, find, findIndex, has, isEqual, isObject, uniq, uniqBy } from 'lodash' import { makeAutoObservable } from 'mobx' import toast from 'react-hot-toast' import { toast as UiToast } from 'ui' @@ -10,8 +9,15 @@ import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_SORT_BY, + STORAGE_SORT_BY_ORDER, STORAGE_VIEWS, } from 'components/to-be-cleaned/Storage/Storage.constants' +import { + StorageColumn, + StorageItem, + StorageItemMetadata, + StorageItemWithColumn, +} from 'components/to-be-cleaned/Storage/Storage.types' import { convertFromBytes } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils' import { ToastLoader } from 'components/ui/ToastLoader' import { configKeys } from 'data/config/keys' @@ -22,17 +28,20 @@ import { downloadBucketObject } from 'data/storage/bucket-object-download-mutati import { getPublicUrlForBucketObject } from 'data/storage/bucket-object-get-public-url-mutation' import { signBucketObject } from 'data/storage/bucket-object-sign-mutation' import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation' +import { Bucket } from 'data/storage/buckets-query' import { moveStorageObject } from 'data/storage/object-move-mutation' -import { API_URL, IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM } from 'lib/constants' import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants' +type CachedFile = { id: string; fetchedAt: number; expiresIn: number; url: string } + /** * This is a preferred method rather than React Context and useStorageExplorerStore(). * If we can switch to this method, we can remove the implementation below, and we don't need compose() within the react components */ -let store = null +let store: StorageExplorerStore | null = null export function useStorageStore() { - if (store === null) store = new StorageExplorerStore(null) + if (store === null) store = new StorageExplorerStore() return store } @@ -46,44 +55,44 @@ const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder' const STORAGE_PROGRESS_INFO_TEXT = "Please do not close the browser until it's completed" class StorageExplorerStore { - projectRef = '' - loaded = false - view = STORAGE_VIEWS.COLUMNS - sortBy = STORAGE_SORT_BY.NAME - sortByOrder = 'asc' - buckets = [] - selectedBucket: { id?: string } = {} - columns = [] - openedFolders = [] - selectedItems = [] - selectedItemsToDelete = [] - selectedItemsToMove = [] - selectedFilePreview = {} - selectedFileCustomExpiry = undefined - - DEFAULT_OPTIONS = { + private projectRef: string = '' + view: STORAGE_VIEWS = STORAGE_VIEWS.COLUMNS + sortBy: STORAGE_SORT_BY = STORAGE_SORT_BY.NAME + sortByOrder: STORAGE_SORT_BY_ORDER = STORAGE_SORT_BY_ORDER.ASC + // selectedBucket will get initialized with a bucket before using + selectedBucket: Bucket = {} as Bucket + columns: StorageColumn[] = [] + openedFolders: StorageItem[] = [] + selectedItems: StorageItemWithColumn[] = [] + selectedItemsToDelete: StorageItemWithColumn[] = [] + selectedItemsToMove: StorageItemWithColumn[] = [] + selectedFilePreview: (StorageItemWithColumn & { previewUrl: string | undefined }) | null = null + selectedFileCustomExpiry: StorageItem | undefined = undefined + + private DEFAULT_OPTIONS = { limit: LIMIT, offset: OFFSET, sortBy: { column: this.sortBy, order: this.sortByOrder }, } - /* Supabase client */ - supabaseClient = null - /* [Joshen] Move towards using API */ - endpoint = '' + /* Supabase client, will get initialized immediately after constructing the instance */ + supabaseClient: SupabaseClient = null as any as SupabaseClient< + any, + 'public', + any + > /* FE Cacheing for file previews */ - filePreviewCache = [] + private filePreviewCache: CachedFile[] = [] /* For file uploads, from 0 to 1 */ - uploadProgress = 0 + private uploadProgress: number = 0 /* Controllers to abort API calls */ - abortController: AbortController | null = null + private abortController: AbortController | null = null - constructor(projectRef) { + constructor() { makeAutoObservable(this, { supabaseClient: false }) - this.projectRef = projectRef // ignore when in a non-browser environment if (typeof window !== 'undefined') { @@ -91,15 +100,23 @@ class StorageExplorerStore { } } - initStore(projectRef, url, serviceKey, protocol = PROJECT_ENDPOINT_PROTOCOL) { + initStore( + projectRef: string, + url: string, + serviceKey: string, + protocol: string = PROJECT_ENDPOINT_PROTOCOL + ) { this.projectRef = projectRef - this.endpoint = `${API_URL}/storage/${projectRef}` if (serviceKey !== undefined) this.initializeSupabaseClient(serviceKey, url, protocol) } /* Methods which are commonly used + For better readability */ - initializeSupabaseClient = (serviceKey, serviceEndpoint, protocol) => { + private initializeSupabaseClient = ( + serviceKey: string, + serviceEndpoint: string, + protocol: string + ) => { this.supabaseClient = createClient( `${IS_PLATFORM ? 'https' : protocol}://${serviceEndpoint}`, serviceKey, @@ -107,11 +124,10 @@ class StorageExplorerStore { auth: { persistSession: false, autoRefreshToken: false, - multiTab: false, detectSessionInUrl: false, - localStorage: { + storage: { getItem: (key) => { - return undefined + return null }, setItem: (key, value) => {}, removeItem: (key) => {}, @@ -121,7 +137,7 @@ class StorageExplorerStore { ) } - updateFileInPreviewCache = (fileCache) => { + private updateFileInPreviewCache = (fileCache: CachedFile) => { const updatedFilePreviewCache = this.filePreviewCache.map((file) => { if (file.id === fileCache.id) return fileCache return file @@ -129,29 +145,21 @@ class StorageExplorerStore { this.filePreviewCache = updatedFilePreviewCache } - addFileToPreviewCache = (fileCache) => { + private addFileToPreviewCache = (fileCache: CachedFile) => { const updatedFilePreviewCache = this.filePreviewCache.concat([fileCache]) this.filePreviewCache = updatedFilePreviewCache } - clearFilePreviewCache = () => { - this.filePreviewCache = [] - } - - getLocalStorageKey = () => { + private getLocalStorageKey = () => { return `supabase-storage-${this.projectRef}` } - getLatestColumnIndex = () => { + private getLatestColumnIndex = () => { return this.columns.length - 1 } - getCurrentlySelectedBucket = () => { - return this.columns.length > 1 ? this.columns[1] : null - } - // Probably refactor this to ignore bucket by default - getPathAlongOpenedFolders = (includeBucket = true) => { + private getPathAlongOpenedFolders = (includeBucket = true) => { if (includeBucket) { return this.openedFolders.length > 0 ? `${this.selectedBucket.name}/${this.openedFolders.map((folder) => folder.name).join('/')}` @@ -160,8 +168,8 @@ class StorageExplorerStore { return this.openedFolders.map((folder) => folder.name).join('/') } - abortApiCalls = () => { - this.abortController.abort() + private abortApiCalls = () => { + this.abortController?.abort() this.abortController = new AbortController() } @@ -171,42 +179,34 @@ class StorageExplorerStore { /* UI specific methods */ - setLoaded = (val) => { - this.loaded = val - } - - setSelectedBucket = (bucket) => { + private setSelectedBucket = (bucket: Bucket) => { this.selectedBucket = bucket this.clearOpenedFolders() this.closeFilePreview() this.clearSelectedItems() } - setView = (view) => { + setView = (view: STORAGE_VIEWS) => { this.view = view this.closeFilePreview() this.updateExplorerPreferences() } - setSortBy = async (sortBy) => { + setSortBy = async (sortBy: STORAGE_SORT_BY) => { this.sortBy = sortBy this.closeFilePreview() this.updateExplorerPreferences() await this.refetchAllOpenedFolders() } - setSortByOrder = async (sortByOrder) => { + setSortByOrder = async (sortByOrder: STORAGE_SORT_BY_ORDER) => { this.sortByOrder = sortByOrder this.closeFilePreview() this.updateExplorerPreferences() await this.refetchAllOpenedFolders() } - clearColumns = () => { - this.columns = [] - } - - pushColumnAtIndex = (column, index) => { + private pushColumnAtIndex = (column: StorageColumn, index: number) => { this.columns = this.columns.slice(0, index + 1).concat([column]) } @@ -215,17 +215,17 @@ class StorageExplorerStore { this.columns = this.columns.slice(0, this.getLatestColumnIndex()) } - popColumnAtIndex = (index) => { + popColumnAtIndex = (index: number) => { this.columns = this.columns.slice(0, index + 1) } - setColumnIsLoadingMore = (index, isLoadingMoreItems = true) => { + private setColumnIsLoadingMore = (index: number, isLoadingMoreItems: boolean = true) => { this.columns = this.columns.map((col, idx) => { return idx === index ? { ...col, isLoadingMoreItems } : col }) } - pushOpenedFolderAtIndex = (folder, index) => { + pushOpenedFolderAtIndex = (folder: StorageItem, index: number) => { this.openedFolders = this.openedFolders.slice(0, index).concat(folder) } @@ -233,7 +233,7 @@ class StorageExplorerStore { this.openedFolders = this.openedFolders.slice(0, this.openedFolders.length - 1) } - popOpenedFoldersAtIndex = (index) => { + popOpenedFoldersAtIndex = (index: number) => { this.openedFolders = this.openedFolders.slice(0, index + 1) } @@ -241,11 +241,11 @@ class StorageExplorerStore { this.openedFolders = [] } - setSelectedItems = (items) => { + setSelectedItems = (items: StorageItemWithColumn[]) => { this.selectedItems = items } - clearSelectedItems = (columnIndex) => { + clearSelectedItems = (columnIndex?: number) => { if (columnIndex !== undefined) { this.selectedItems = this.selectedItems.filter((item) => item.columnIndex !== columnIndex) } else { @@ -253,7 +253,7 @@ class StorageExplorerStore { } } - setSelectedItemsToDelete = (items) => { + setSelectedItemsToDelete = (items: StorageItemWithColumn[]) => { this.selectedItemsToDelete = items } @@ -261,7 +261,7 @@ class StorageExplorerStore { this.selectedItemsToDelete = [] } - setSelectedItemsToMove = (items) => { + setSelectedItemsToMove = (items: StorageItemWithColumn[]) => { this.selectedItemsToMove = items } @@ -269,19 +269,19 @@ class StorageExplorerStore { this.selectedItemsToMove = [] } - setSelectedFileCustomExpiry = (item) => { + setSelectedFileCustomExpiry = (item: StorageItem | undefined) => { this.selectedFileCustomExpiry = item } - addNewFolderPlaceholder = (columnIndex) => { + addNewFolderPlaceholder = (columnIndex: number) => { const isPrepend = true const folderName = 'Untitled folder' const folderType = STORAGE_ROW_TYPES.FOLDER const columnIdx = columnIndex === -1 ? this.getLatestColumnIndex() : columnIndex - this.addTempRow(folderType, folderName, STORAGE_ROW_STATUS.EDITING, columnIdx, {}, isPrepend) + this.addTempRow(folderType, folderName, STORAGE_ROW_STATUS.EDITING, columnIdx, null, isPrepend) } - addNewFolder = async (folderName, columnIndex) => { + addNewFolder = async (folderName: string, columnIndex: number) => { const autofix = false const formattedName = this.sanitizeNameForDuplicateInColumn(folderName, autofix, columnIndex) if (formattedName === null) return @@ -321,7 +321,7 @@ class StorageExplorerStore { } } - setFilePreview = async (file) => { + setFilePreview = async (file: StorageItemWithColumn) => { const size = file.metadata?.size const mimeType = file.metadata?.mimetype @@ -334,7 +334,7 @@ class StorageExplorerStore { // Either retrieve file preview from FE cache or retrieve signed url this.selectedFilePreview = { ...file, previewUrl: 'loading' } - const cachedPreview = find(this.filePreviewCache, { id: file.id }) + const cachedPreview = this.filePreviewCache.find((cache) => cache.id === file.id) const fetchedAt = cachedPreview?.fetchedAt ?? null const expiresIn = cachedPreview?.expiresIn ?? null @@ -342,7 +342,7 @@ class StorageExplorerStore { const isExpired = existsInCache ? fetchedAt + expiresIn * 1000 < Date.now() : true if (!isExpired) { - this.selectedFilePreview = { ...file, previewUrl: cachedPreview.url } + this.selectedFilePreview = { ...file, previewUrl: cachedPreview?.url } } else { const previewUrl = await this.fetchFilePreview(file.name) const formattedPreviewUrl = this.selectedBucket.public @@ -350,8 +350,8 @@ class StorageExplorerStore { : previewUrl this.selectedFilePreview = { ...file, previewUrl: formattedPreviewUrl } - const fileCache = { - id: file.id, + const fileCache: CachedFile = { + id: file.id as string, url: previewUrl, expiresIn: DEFAULT_EXPIRY, fetchedAt: Date.now(), @@ -363,28 +363,28 @@ class StorageExplorerStore { } } } else { - this.selectedFilePreview = { ...file, previewUrl: null } + this.selectedFilePreview = { ...file, previewUrl: undefined } } } closeFilePreview = () => { - this.selectedFilePreview = {} + this.selectedFilePreview = null } - getFileUrl = async (file, expiresIn = 0) => { - const filePreview = find(this.filePreviewCache, { id: file.id }) + getFileUrl = async (file: StorageItem, expiresIn = 0) => { + const filePreview = this.filePreviewCache.find((cache) => cache.id === file.id) if (filePreview !== undefined && expiresIn === 0) { return filePreview.url } else { const signedUrl = await this.fetchFilePreview(file.name, expiresIn) try { - const formattedUrl = new URL(signedUrl) + const formattedUrl = new URL(signedUrl!) formattedUrl.searchParams.set('t', new Date().toISOString()) const fileUrl = formattedUrl.toString() // Also save it to cache - const fileCache = { - id: file.id, + const fileCache: CachedFile = { + id: file.id as string, url: fileUrl, expiresIn: DEFAULT_EXPIRY, fetchedAt: Date.now(), @@ -400,7 +400,7 @@ class StorageExplorerStore { /* Methods that involve the storage client library */ /* Bucket CRUD */ - openBucket = async (bucket) => { + openBucket = async (bucket: Bucket) => { const { id, name } = bucket const columnIndex = -1 if (!isEqual(this.selectedBucket, bucket)) { @@ -411,7 +411,7 @@ class StorageExplorerStore { /* Files CRUD */ - getFile = async (fileEntry) => { + private getFile = async (fileEntry: FileSystemFileEntry): Promise => { try { return await new Promise((resolve, reject) => fileEntry.file(resolve, reject)) } catch (err) { @@ -421,23 +421,28 @@ class StorageExplorerStore { } // https://stackoverflow.com/a/53058574 - getFilesDataTransferItems = async (items) => { + private getFilesDataTransferItems = async (items: DataTransferItemList) => { const { dismiss } = UiToast({ description: 'Retrieving items to upload...' }) - const files = [] - const queue = [] + const files: (File & { path: string })[] = [] + const queue: FileSystemEntry[] = [] for (const item of items) { - queue.push(item.webkitGetAsEntry()) + const entry = item.webkitGetAsEntry() + if (entry) { + queue.push(entry) + } } while (queue.length > 0) { - const entry = queue.shift() || {} - if (entry.isFile) { - const file = await this.getFile(entry) + const entry = queue.shift() + if (entry && entry.isFile) { + const fileEntry = entry as FileSystemFileEntry + const file = await this.getFile(fileEntry) if (file !== undefined) { - file.path = entry.fullPath.slice(1) - files.push(file) + ;(file as any).path = fileEntry.fullPath.slice(1) + files.push(file as File & { path: string }) } - } else if (entry.isDirectory) { - queue.push(...(await this.readAllDirectoryEntries(entry.createReader()))) + } else if (entry && entry.isDirectory) { + const dirEntry = entry as FileSystemDirectoryEntry + queue.push(...(await this.readAllDirectoryEntries(dirEntry.createReader()))) } } dismiss() @@ -446,10 +451,10 @@ class StorageExplorerStore { // Get all the entries (files or sub-directories) in a directory // by calling readEntries until it returns empty array - readAllDirectoryEntries = async (directoryReader) => { + private readAllDirectoryEntries = async (directoryReader: FileSystemDirectoryReader) => { const entries = [] let readEntries = await this.readEntriesPromise(directoryReader) - while (readEntries.length > 0) { + while (readEntries && readEntries.length > 0) { entries.push(...readEntries) readEntries = await this.readEntriesPromise(directoryReader) } @@ -459,9 +464,9 @@ class StorageExplorerStore { // Wrap readEntries in a promise to make working with readEntries easier // readEntries will return only some of the entries in a directory // e.g. Chrome returns at most 100 entries at a time - readEntriesPromise = async (directoryReader) => { + private readEntriesPromise = async (directoryReader: FileSystemDirectoryReader) => { try { - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { directoryReader.readEntries(resolve, reject) }) } catch (err) { @@ -469,7 +474,11 @@ class StorageExplorerStore { } } - uploadFiles = async (files, columnIndex, isDrop = false) => { + uploadFiles = async ( + files: FileList | DataTransferItemList, + columnIndex: number, + isDrop: boolean = false + ) => { const queryClient = getQueryClient() const storageConfiguration = queryClient .getQueryCache() @@ -482,9 +491,11 @@ class StorageExplorerStore { const autofix = true // We filter out any folders which are just '#' until we can properly encode such characters in the URL - const filesToUpload = isDrop - ? (await this.getFilesDataTransferItems(files)).filter((file) => !file.path.includes('#/')) - : Array.from(files) + const filesToUpload: (File & { path?: string })[] = isDrop + ? (await this.getFilesDataTransferItems(files as DataTransferItemList)).filter( + (file) => !file.path.includes('#/') + ) + : Array.from(files as FileList) const derivedColumnIndex = columnIndex === -1 ? this.getLatestColumnIndex() : columnIndex const filesWithinUploadLimit = @@ -494,7 +505,7 @@ class StorageExplorerStore { if (filesWithinUploadLimit.length < filesToUpload.length) { const numberOfFilesRejected = filesToUpload.length - filesWithinUploadLimit.length - const { value, unit } = convertFromBytes(fileSizeLimit) + const { value, unit } = convertFromBytes(fileSizeLimit as number) toast.error(

@@ -516,7 +527,7 @@ class StorageExplorerStore { // If we're uploading a folder which name already exists in the same folder that we're uploading to // We sanitize the folder name and let all file uploads through. (This is only via drag drop) - const topLevelFolders = (this.columns?.[derivedColumnIndex]?.items ?? []) + const topLevelFolders: string[] = (this.columns?.[derivedColumnIndex]?.items ?? []) .filter((item) => !item.id) .map((item) => item.name) const formattedFilesToUpload = filesWithinUploadLimit.map((file) => { @@ -526,20 +537,20 @@ class StorageExplorerStore { const path = file.path.split('/') const topLevelFolder = path.length > 1 ? path[0] : null - if (topLevelFolders.includes(topLevelFolder)) { + if (topLevelFolders.includes(topLevelFolder as string)) { const newTopLevelFolder = this.sanitizeNameForDuplicateInColumn( - topLevelFolder, + topLevelFolder as string, autofix, columnIndex ) - path[0] = newTopLevelFolder + path[0] = newTopLevelFolder as string file.path = path.join('/') } return file }) this.uploadProgress = 0 - const uploadedTopLevelFolders = [] + const uploadedTopLevelFolders: string[] = [] const numberOfFilesToUpload = formattedFilesToUpload.length let numberOfFilesUploadedSuccess = 0 let numberOfFilesUploadedFail = 0 @@ -562,18 +573,18 @@ class StorageExplorerStore { // Upload files in batches const promises = formattedFilesToUpload.map((file) => { const fileOptions = { cacheControl: '3600' } - const metadata = { mimetype: file.type, size: file.size } + const metadata = { mimetype: file.type, size: file.size } as StorageItemMetadata const isWithinFolder = (file?.path ?? '').split('/').length > 1 const fileName = !isWithinFolder ? this.sanitizeNameForDuplicateInColumn(file.name, autofix) : file.name - const formattedFileName = has(file, ['path']) && isWithinFolder ? file.path : fileName + const formattedFileName = file.path && isWithinFolder ? file.path : fileName const formattedPathToFile = - pathToFile.length > 0 ? `${pathToFile}/${formattedFileName}` : formattedFileName + pathToFile.length > 0 ? `${pathToFile}/${formattedFileName}` : (formattedFileName as string) if (isWithinFolder) { - const topLevelFolder = file.path.split('/')[0] + const topLevelFolder = file.path?.split('/')[0] || '' if (!uploadedTopLevelFolders.includes(topLevelFolder)) { this.addTempRow( STORAGE_ROW_TYPES.FOLDER, @@ -587,7 +598,7 @@ class StorageExplorerStore { } else { this.addTempRow( STORAGE_ROW_TYPES.FILE, - fileName, + fileName!, STORAGE_ROW_STATUS.LOADING, derivedColumnIndex, metadata @@ -595,7 +606,7 @@ class StorageExplorerStore { } return () => { - return new Promise(async (resolve) => { + return new Promise(async (resolve) => { const { error } = await this.supabaseClient.storage .from(this.selectedBucket.name) .upload(formattedPathToFile, file, fileOptions) @@ -673,11 +684,11 @@ class StorageExplorerStore { const t2 = new Date() console.log( - `Total time taken for ${formattedFilesToUpload.length} files: ${(t2 - t1) / 1000} seconds` + `Total time taken for ${formattedFilesToUpload.length} files: ${((t2 as any) - (t1 as any)) / 1000} seconds` ) } - moveFiles = async (newPathToFile) => { + moveFiles = async (newPathToFile: string) => { const newPaths = compact(newPathToFile.split('/')) const formattedNewPathToFile = newPaths.join('/') let numberOfFilesMovedFail = 0 @@ -736,7 +747,7 @@ class StorageExplorerStore { this.clearSelectedItemsToMove() } - fetchFilePreview = async (fileName, expiresIn = 0): Promise => { + private fetchFilePreview = async (fileName: string, expiresIn: number = 0) => { const includeBucket = false const pathToFile = this.getPathAlongOpenedFolders(includeBucket) const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName @@ -765,16 +776,20 @@ class StorageExplorerStore { toast.error(`Failed to fetch signed url preview: ${error.message}`) } } - return null + return '' } - deleteFiles = async (files, isDeleteFolder = false) => { + // the method accepts either files with column index or with prefix. + deleteFiles = async ( + files: (StorageItemWithColumn & { prefix?: string })[], + isDeleteFolder = false + ) => { this.closeFilePreview() let progress = 0 // If every file has the 'prefix' property, then just construct the prefix // directly (from delete folder). Otherwise go by the opened folders. - const prefixes = !some(files, 'prefix') + const prefixes = !files.some((f) => f.prefix) ? files.map((file) => { const { name, columnIndex } = file const pathToFile = this.openedFolders @@ -846,7 +861,7 @@ class StorageExplorerStore { } } - downloadFolder = async (folder) => { + downloadFolder = async (folder: StorageItemWithColumn) => { let progress = 0 const toastId = toast.loading('Retrieving files from folder...') @@ -856,7 +871,7 @@ class StorageExplorerStore { 1 ? 's' : ''}...`} - desription={STORAGE_PROGRESS_INFO_TEXT} + description={STORAGE_PROGRESS_INFO_TEXT} />, { id: toastId } ) @@ -864,7 +879,14 @@ class StorageExplorerStore { const promises = files.map((file) => { const fileMimeType = file.metadata?.mimetype ?? null return () => { - return new Promise(async (resolve) => { + return new Promise< + | { + name: string + prefix: string + blob: Blob + } + | boolean + >(async (resolve) => { try { const data = await downloadBucketObject({ projectRef: this.projectRef, @@ -888,19 +910,28 @@ class StorageExplorerStore { }) 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())) - toast.loading( - 1 ? 's' : ''}...`} - desription={STORAGE_PROGRESS_INFO_TEXT} - />, - { id: toastId } - ) - return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean)) - }, Promise.resolve()) + const downloadedFiles = await batchedPromises.reduce( + async (previousPromise, nextBatch) => { + const previousResults = await previousPromise + const batchResults = await Promise.allSettled(nextBatch.map((batch) => batch())) + toast.loading( + 1 ? 's' : ''}...`} + description={STORAGE_PROGRESS_INFO_TEXT} + />, + { id: toastId } + ) + return previousResults.concat(batchResults.map((x: any) => x.value).filter(Boolean)) + }, + Promise.resolve< + { + name: string + prefix: string + blob: Blob + }[] + >([]) + ) const zipFileWriter = new BlobWriter('application/zip') const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true }) @@ -919,7 +950,7 @@ class StorageExplorerStore { link.setAttribute('download', `${folder.name}.zip`) document.body.appendChild(link) link.click() - link.parentNode.removeChild(link) + link.parentNode?.removeChild(link) toast.success( downloadedFiles.length === files.length @@ -931,10 +962,10 @@ class StorageExplorerStore { ) } - downloadSelectedFiles = async (files) => { + downloadSelectedFiles = async (files: StorageItemWithColumn[]) => { const lowestColumnIndex = Math.min(...files.map((file) => file.columnIndex)) - const formattedFilesWithPrefix = files.map((file) => { + const formattedFilesWithPrefix: any[] = files.map((file) => { const { name, columnIndex } = file const pathToFile = this.openedFolders .slice(lowestColumnIndex, columnIndex) @@ -953,10 +984,13 @@ class StorageExplorerStore { const promises = formattedFilesWithPrefix.map((file) => { return () => { - return new Promise(async (resolve) => { + return new Promise<{ name: string; blob: Blob } | boolean>(async (resolve) => { const data = await this.downloadFile(file, showIndividualToast, returnBlob) progress = progress + 1 / formattedFilesWithPrefix.length - resolve({ ...data, name: file.formattedPathToFile }) + if (isObject(data)) { + resolve({ ...data, name: file.formattedPathToFile }) + } + resolve(false) }) } }) @@ -973,8 +1007,8 @@ class StorageExplorerStore { />, { id: toastId } ) - return (previousResults ?? []).concat(batchResults.map((x) => x.value).filter(Boolean)) - }, Promise.resolve()) + return previousResults.concat(batchResults.map((x: any) => x.value).filter(Boolean)) + }, Promise.resolve<{ name: string; blob: Blob }[]>([])) const zipFileWriter = new BlobWriter('application/zip') const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true }) @@ -988,14 +1022,14 @@ class StorageExplorerStore { link.setAttribute('download', `supabase-files.zip`) document.body.appendChild(link) link.click() - link.parentNode.removeChild(link) + link.parentNode?.removeChild(link) toast.success(`Successfully downloaded ${downloadedFiles.length} files`, { id: toastId }) } - downloadFile = async (file, showToast = true, returnBlob = false) => { - const fileName = file.name - const fileMimeType = file?.metadata?.mimetype ?? null + downloadFile = async (file: StorageItemWithColumn, showToast = true, returnBlob = false) => { + const fileName: string = file.name + const fileMimeType = file?.metadata?.mimetype ?? undefined const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined @@ -1022,7 +1056,7 @@ class StorageExplorerStore { link.setAttribute('download', `${fileName}`) document.body.appendChild(link) link.click() - link.parentNode.removeChild(link) + link.parentNode?.removeChild(link) window.URL.revokeObjectURL(blob) if (toastId) { toast.success(`Downloading ${fileName}`, { id: toastId }) @@ -1036,7 +1070,7 @@ class StorageExplorerStore { } } - renameFile = async (file, newName, columnIndex) => { + renameFile = async (file: StorageItem, newName: string, columnIndex: number) => { const originalName = file.name if (originalName === newName || newName.length === 0) { this.updateRowStatus(originalName, STORAGE_ROW_STATUS.READY, columnIndex) @@ -1064,8 +1098,8 @@ class StorageExplorerStore { ) this.filePreviewCache = updatedFilePreviewCache - if (this.selectedFilePreview.name === originalName) { - const { previewUrl, ...fileData } = file + if (this.selectedFilePreview?.name === originalName) { + const { previewUrl, ...fileData } = file as any this.setFilePreview({ ...fileData, name: newName }) } @@ -1078,7 +1112,12 @@ class StorageExplorerStore { /* Folders CRUD */ - fetchFolderContents = async (folderId, folderName, index, searchString = '') => { + fetchFolderContents = async ( + folderId: string | null, + folderName: string, + index: number, + searchString: string = '' + ) => { if (this.selectedBucket.id === undefined) return this.abortApiCalls() @@ -1117,6 +1156,7 @@ class StorageExplorerStore { { id: folderId || folderName, name: folderName, + status: STORAGE_ROW_STATUS.READY, items: formattedItems, hasMoreItems: formattedItems.length === LIMIT, isLoadingMoreItems: false, @@ -1130,7 +1170,11 @@ class StorageExplorerStore { } } - fetchMoreFolderContents = async (index, column, searchString = '') => { + fetchMoreFolderContents = async ( + index: number, + column: StorageColumn, + searchString: string = '' + ) => { this.setColumnIsLoadingMore(index) const prefix = this.openedFolders.map((folder) => folder.name).join('/') @@ -1172,7 +1216,11 @@ class StorageExplorerStore { await this.fetchFoldersByPath(paths) } - fetchFoldersByPath = async (paths, searchString = '', showLoading = false) => { + fetchFoldersByPath = async ( + paths: string[], + searchString: string = '', + showLoading: boolean = false + ) => { if (this.selectedBucket.id === undefined) return const pathsWithEmptyPrefix = [''].concat(paths) @@ -1212,6 +1260,7 @@ class StorageExplorerStore { const formattedItems = this.formatFolderItems(folderItems) return { id: null, + status: STORAGE_ROW_STATUS.READY, name: idx === 0 ? this.selectedBucket.name : pathsWithEmptyPrefix[idx], items: formattedItems, hasMoreItems: formattedItems.length === LIMIT, @@ -1223,7 +1272,7 @@ class StorageExplorerStore { this.columns = formattedFolders // Update openedFolders as well - const updatedOpenedFolders = paths.map((path, idx) => { + const updatedOpenedFolders: StorageItem[] = paths.map((path, idx) => { const folderInfo = find(formattedFolders[idx].items, { name: path }) // Folder doesnt exist, FE just scaffolds a "fake" folder if (!folderInfo) { @@ -1232,6 +1281,11 @@ class StorageExplorerStore { name: path, type: STORAGE_ROW_TYPES.FOLDER, status: STORAGE_ROW_STATUS.READY, + metadata: null, + isCorrupted: false, + created_at: null, + updated_at: null, + last_accessed_at: null, } } return folderInfo @@ -1241,7 +1295,7 @@ class StorageExplorerStore { // Check parent folder if its empty, if yes, reinstate .emptyFolderPlaceholder // Used when deleting folder or deleting files - validateParentFolderEmpty = async (parentFolderPrefix) => { + private validateParentFolderEmpty = async (parentFolderPrefix: string) => { try { const data = await listBucketObjects({ projectRef: this.projectRef, @@ -1259,10 +1313,10 @@ class StorageExplorerStore { } catch (error) {} } - deleteFolder = async (folder) => { + deleteFolder = async (folder: StorageItemWithColumn) => { const isDeleteFolder = true const files = await this.getAllItemsAlongFolder(folder) - await this.deleteFiles(files, isDeleteFolder) + await this.deleteFiles(files as any[], isDeleteFolder) this.popColumnAtIndex(folder.columnIndex) this.popOpenedFoldersAtIndex(folder.columnIndex - 1) @@ -1281,7 +1335,7 @@ class StorageExplorerStore { toast.success(`Successfully deleted ${folder.name}`) } - renameFolder = async (folder, newName, columnIndex) => { + renameFolder = async (folder: StorageItemWithColumn, newName: string, columnIndex: number) => { const originalName = folder.name if (originalName === newName) { return this.updateRowStatus(originalName, STORAGE_ROW_STATUS.READY, columnIndex) @@ -1323,7 +1377,7 @@ class StorageExplorerStore { .concat(pathSegments.slice(columnIndex + 1)) .join('/') return () => { - return new Promise(async (resolve) => { + return new Promise(async (resolve) => { progress = progress + 1 / files.length try { await moveStorageObject({ @@ -1348,7 +1402,7 @@ class StorageExplorerStore { await batchedPromises.reduce(async (previousPromise, nextBatch) => { await previousPromise await Promise.all(nextBatch.map((batch) => batch())) - toast.loader( + toast.loading( { - const items = [] + private getAllItemsAlongFolder = async (folder: { + name: string + columnIndex: number + prefix?: string + }): Promise<(StorageObject & { prefix: string })[]> => { + const items: (StorageObject & { prefix: string })[] = [] let formattedPathToFolder = '' const { name, columnIndex, prefix } = folder @@ -1406,7 +1464,7 @@ class StorageExplorerStore { offset: OFFSET, sortBy: { column: this.sortBy, order: this.sortByOrder }, } - let folderContents = [] + let folderContents: StorageObject[] = [] for (;;) { try { @@ -1431,7 +1489,7 @@ class StorageExplorerStore { const subFolderContents = await Promise.all( subfolders.map((folder) => - this.getAllItemsAlongFolder({ ...folder, prefix: formattedPathToFolder }) + this.getAllItemsAlongFolder({ ...folder, columnIndex: 0, prefix: formattedPathToFolder }) ) ) subFolderContents.map((subfolderContent) => { @@ -1443,10 +1501,10 @@ class StorageExplorerStore { /* UI Helper functions */ - sanitizeNameForDuplicateInColumn = ( - name, - autofix = false, - columnIndex = this.getLatestColumnIndex() + private sanitizeNameForDuplicateInColumn = ( + name: string, + autofix: boolean = false, + columnIndex: number = this.getLatestColumnIndex() ) => { const currentColumn = this.columns[columnIndex] const currentColumnItems = currentColumn.items.filter( @@ -1477,7 +1535,7 @@ class StorageExplorerStore { return name } - formatFolderItems = (items: StorageObject[] = []) => { + private formatFolderItems = (items: StorageObject[] = []): StorageItem[] => { const formattedItems = (items ?? []) ?.filter((item) => item.name !== EMPTY_FOLDER_PLACEHOLDER_FILE_NAME) @@ -1497,16 +1555,29 @@ class StorageExplorerStore { ? STORAGE_ROW_STATUS.LOADING : STORAGE_ROW_STATUS.READY - const itemObj = { ...item, type, status, isCorrupted } + const itemObj = { + ...item, + metadata: item.metadata as any as StorageItemMetadata, + type, + status, + isCorrupted, + } return itemObj }) ?? [] return formattedItems } - addTempRow = (type, name, status, columnIndex, metadata = {}, isPrepend = false) => { + private addTempRow = ( + type: STORAGE_ROW_TYPES, + name: string, + status: STORAGE_ROW_STATUS, + columnIndex: number, + metadata: StorageItemMetadata | null, + isPrepend: boolean = false + ) => { const updatedColumns = this.columns.map((column, idx) => { if (idx === columnIndex) { - const tempRow = { type, name, status, metadata } + const tempRow = { type, name, status, metadata } as StorageItem const updatedItems = isPrepend ? [tempRow].concat(column.items) : column.items.concat([tempRow]) @@ -1517,7 +1588,7 @@ class StorageExplorerStore { this.columns = updatedColumns } - removeTempRows = (columnIndex) => { + private removeTempRows = (columnIndex: number) => { const updatedColumns = this.columns.map((column, idx) => { if (idx === columnIndex) { const updatedItems = column.items.filter((item) => has(item, 'id')) @@ -1528,15 +1599,15 @@ class StorageExplorerStore { this.columns = updatedColumns } - setSelectedItemToRename = (file) => { + setSelectedItemToRename = (file: { name: string; columnIndex: number }) => { this.updateRowStatus(file.name, STORAGE_ROW_STATUS.EDITING, file.columnIndex) } - updateRowStatus = ( - name, - status, - columnIndex = this.getLatestColumnIndex(), - updatedName = null + private updateRowStatus = ( + name: string, + status: STORAGE_ROW_STATUS, + columnIndex: number = this.getLatestColumnIndex(), + updatedName?: string ) => { const updatedColumns = this.columns.map((column, idx) => { if (idx === columnIndex) { @@ -1557,10 +1628,10 @@ class StorageExplorerStore { this.columns = updatedColumns } - updateFolderAfterEdit = ( - folderName, - columnIndex = this.getLatestColumnIndex(), - status = STORAGE_ROW_STATUS.READY + private updateFolderAfterEdit = ( + folderName: string, + columnIndex: number = this.getLatestColumnIndex(), + status: STORAGE_ROW_STATUS = STORAGE_ROW_STATUS.READY ) => { const updatedColumns = this.columns.map((column, idx) => { if (idx === columnIndex) { @@ -1589,7 +1660,7 @@ class StorageExplorerStore { /* User Preferences */ - updateExplorerPreferences = () => { + private updateExplorerPreferences = () => { const localStorageKey = this.getLocalStorageKey() const preferences = { view: this.view, @@ -1616,7 +1687,7 @@ class StorageExplorerStore { } } - selectRangeItems = (columnIndex, toItemIndex) => { + selectRangeItems = (columnIndex: number, toItemIndex: number) => { const columnItems = this.columns[columnIndex].items const toItem = columnItems[toItemIndex] const selectedItemIds = this.selectedItems.map((item) => item.id) From af6fe87b7cadb08cf1b18cd1859c92bc9e16754f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 6 Jun 2024 16:45:52 +0700 Subject: [PATCH 5/6] Update all mutations for storage explorer to follow template syntax --- .../storage/bucket-object-delete-mutation.ts | 40 +++++++++++++++---- .../bucket-object-download-mutation.ts | 35 +++++++++++++--- .../bucket-object-get-public-url-mutation.ts | 38 ++++++++++++++---- .../storage/bucket-object-sign-mutation.ts | 38 ++++++++++++++---- .../storage/bucket-objects-list-mutation.ts | 35 +++++++++++++--- .../data/storage/object-move-mutation.ts | 34 +++++++++++++--- 6 files changed, 184 insertions(+), 36 deletions(-) diff --git a/apps/studio/data/storage/bucket-object-delete-mutation.ts b/apps/studio/data/storage/bucket-object-delete-mutation.ts index 0484968eeae46..02dab04562ded 100644 --- a/apps/studio/data/storage/bucket-object-delete-mutation.ts +++ b/apps/studio/data/storage/bucket-object-delete-mutation.ts @@ -1,14 +1,16 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation, useQueryClient } from '@tanstack/react-query' import { del, handleError } from 'data/fetchers' +import toast from 'react-hot-toast' +import { ResponseError } from 'types' -type DownloadBucketObjectParams = { +type DeleteBucketObjectParams = { projectRef: string bucketId?: string paths: string[] } export const deleteBucketObject = async ( - { projectRef, bucketId, paths }: DownloadBucketObjectParams, + { projectRef, bucketId, paths }: DeleteBucketObjectParams, signal?: AbortSignal ) => { if (!bucketId) throw new Error('bucketId is required') @@ -30,8 +32,32 @@ export const deleteBucketObject = async ( return data } -export function useBucketObjectDeleteMutation() { - return useMutation({ - mutationFn: deleteBucketObject, - }) +type BucketObjectDeleteData = Awaited> + +export const useBucketObjectDeleteMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + return useMutation( + (vars) => deleteBucketObject(vars), + { + async onSuccess(data, variables, context) { + // [Joshen] TODO figure out what queries to invalidate + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to delete bucket object: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } diff --git a/apps/studio/data/storage/bucket-object-download-mutation.ts b/apps/studio/data/storage/bucket-object-download-mutation.ts index 7b8d004fd1634..afbd37ad5df05 100644 --- a/apps/studio/data/storage/bucket-object-download-mutation.ts +++ b/apps/studio/data/storage/bucket-object-download-mutation.ts @@ -1,8 +1,10 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation } from '@tanstack/react-query' +import toast from 'react-hot-toast' import { components } from 'data/api' import { post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' +import { ResponseError } from 'types' type DownloadBucketObjectParams = { projectRef: string @@ -10,6 +12,7 @@ type DownloadBucketObjectParams = { path: string options?: components['schemas']['DownloadObjectOptions'] } + export const downloadBucketObject = async ( { projectRef, bucketId, path, options }: DownloadBucketObjectParams, signal?: AbortSignal @@ -30,8 +33,30 @@ export const downloadBucketObject = async ( return response } -export function useBucketObjectDownloadMutation() { - return useMutation({ - mutationFn: downloadBucketObject, - }) +type BucketObjectDeleteData = Awaited> + +export const useBucketObjectDownloadMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => downloadBucketObject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to download bucket object: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } diff --git a/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts index f0dfe13f4e5b9..c5231e6ee40ac 100644 --- a/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts +++ b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts @@ -1,16 +1,18 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation } from '@tanstack/react-query' +import toast from 'react-hot-toast' import { components } from 'data/api' import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' -type getPublicUrlForBucketObjectParams = { +type BucketObjectPublicUrlParams = { projectRef: string bucketId?: string path: string options?: components['schemas']['PublicUrlOptions'] } export const getPublicUrlForBucketObject = async ( - { projectRef, bucketId, path, options }: getPublicUrlForBucketObjectParams, + { projectRef, bucketId, path, options }: BucketObjectPublicUrlParams, signal?: AbortSignal ) => { if (!bucketId) throw new Error('bucketId is required') @@ -30,8 +32,30 @@ export const getPublicUrlForBucketObject = async ( return data } -export function useBucketObjectGetPublicUrlMutation() { - return useMutation({ - mutationFn: getPublicUrlForBucketObject, - }) +type BucketObjectPublicUrlData = Awaited> + +export const useGetBucketObjectPublicUrlMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => getPublicUrlForBucketObject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to get public URL of bucket object: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } diff --git a/apps/studio/data/storage/bucket-object-sign-mutation.ts b/apps/studio/data/storage/bucket-object-sign-mutation.ts index cf1fb0b2f4146..a0d4c6d5df608 100644 --- a/apps/studio/data/storage/bucket-object-sign-mutation.ts +++ b/apps/studio/data/storage/bucket-object-sign-mutation.ts @@ -1,9 +1,11 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation } from '@tanstack/react-query' +import toast from 'react-hot-toast' import { components } from 'data/api' import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' -type signBucketObjectParams = { +type SignBucketObjectParams = { projectRef: string bucketId?: string path: string @@ -11,7 +13,7 @@ type signBucketObjectParams = { options?: components['schemas']['SignedUrlOptions'] } export const signBucketObject = async ( - { projectRef, bucketId, path, expiresIn, options }: signBucketObjectParams, + { projectRef, bucketId, path, expiresIn, options }: SignBucketObjectParams, signal?: AbortSignal ) => { if (!bucketId) throw new Error('bucketId is required') @@ -31,8 +33,30 @@ export const signBucketObject = async ( return data } -export function useSignBucketObjectMutation() { - return useMutation({ - mutationFn: signBucketObject, - }) +type SignBucketObjectData = Awaited> + +export const useGetSignBucketObjectMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => signBucketObject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to get sign bucket object: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } diff --git a/apps/studio/data/storage/bucket-objects-list-mutation.ts b/apps/studio/data/storage/bucket-objects-list-mutation.ts index 65ff8ed36974e..e8c2ee38e0831 100644 --- a/apps/studio/data/storage/bucket-objects-list-mutation.ts +++ b/apps/studio/data/storage/bucket-objects-list-mutation.ts @@ -1,7 +1,9 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation } from '@tanstack/react-query' +import toast from 'react-hot-toast' import { components } from 'data/api' import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' type ListBucketObjectsParams = { projectRef: string @@ -12,6 +14,7 @@ type ListBucketObjectsParams = { export type StorageObject = components['schemas']['StorageObject'] +// [Joshen] Ideally we transform this into a query that uses a POST i think export const listBucketObjects = async ( { projectRef, bucketId, path, options }: ListBucketObjectsParams, signal?: AbortSignal @@ -36,8 +39,30 @@ export const listBucketObjects = async ( return data } -export function useBucketObjectsListMutation() { - return useMutation({ - mutationFn: listBucketObjects, - }) +type ListBucketObjectsData = Awaited> + +export const useGetSignBucketObjectMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => listBucketObjects(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to list bucket objects: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } diff --git a/apps/studio/data/storage/object-move-mutation.ts b/apps/studio/data/storage/object-move-mutation.ts index 7a17ea1beddd5..fadf0c866feda 100644 --- a/apps/studio/data/storage/object-move-mutation.ts +++ b/apps/studio/data/storage/object-move-mutation.ts @@ -1,6 +1,8 @@ -import { useMutation } from '@tanstack/react-query' +import { UseMutationOptions, useMutation } from '@tanstack/react-query' +import toast from 'react-hot-toast' import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' type MoveStorageObjectParams = { projectRef: string @@ -33,8 +35,30 @@ export const moveStorageObject = async ({ return data } -export function useBucketObjectsMoveMutation() { - return useMutation({ - mutationFn: moveStorageObject, - }) +type MoveBucketObjectData = Awaited> + +export const useGetSignBucketObjectMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => moveStorageObject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to move bucket object: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) } From dfdf32d8f9471d15adf6e0653882a7effe7b9a25 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 7 Jun 2024 13:36:53 +0700 Subject: [PATCH 6/6] Update apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx Co-authored-by: Alaister Young --- .../to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index c1301672b5c2b..d12a2c4dfcd8f 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -118,7 +118,7 @@ const PreviewPane = ({ onCopyUrl }: PreviewPaneProps) => { const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') if (!file) { - return <> + return null } const width = 450