From ff8a4be0e8a5d0330e2c92588627214afcce8164 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Mon, 8 Sep 2025 14:25:45 +1000 Subject: [PATCH 1/9] Fix adding bucket policy to schema (#38499) bucket policy --- .../StoragePolicies/StoragePoliciesBucketRow.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx index 1dffed3424640..dc71182c82fe8 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx @@ -23,7 +23,7 @@ interface StoragePoliciesBucketRowProps { label: string bucket?: Bucket policies: PostgresPolicy[] - onSelectPolicyAdd: (bucketName: string, table: string) => void + onSelectPolicyAdd: (bucketName: string | undefined, table: string) => void onSelectPolicyEdit: (policy: PostgresPolicy, bucketName: string, table: string) => void onSelectPolicyDelete: (policy: PostgresPolicy) => void } @@ -45,11 +45,9 @@ export const StoragePoliciesBucketRow = ({ {label} {bucket?.public && Public} - {!!bucket && ( - - )} + {policies.length === 0 ? ( From 8c6c3eb0d2beb9e5cce664ad8d3f8d435da2526b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 8 Sep 2025 12:38:44 +0800 Subject: [PATCH 2/9] Fix roles (#38501) --- .../UpdateRolesPanel/UpdateRolesPanel.utils.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts index 7b2cb3e2d2703..bc3dfb716274c 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts @@ -42,9 +42,16 @@ export const formatMemberRoleToProjectRoleConfiguration = ( }) .filter(Boolean) .flat() - .filter( - (p) => p !== undefined && 'baseRoleId' in p && p.ref !== undefined - ) as ProjectRoleConfiguration[] + .filter((p) => { + // [Joshen] Validate only for project scoped roles + // This filters out project scoped roles for projects that the user doesn't have access to + // (e.g if the project was deleted) + if ('baseRoleId' in p!) { + return p.ref !== undefined + } else { + return p + } + }) as ProjectRoleConfiguration[] return roleConfiguration } From b9c8b3087cabac0d9c448fd60ea80160832ff0cb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 8 Sep 2025 13:39:13 +0800 Subject: [PATCH 3/9] Swap useCheckPermissions with useAsyncCheckPermissions part 4: Storage (#38458) --- .../StorageExplorer/ColumnContextMenu.tsx | 17 +- .../StorageExplorer/CustomExpiryModal.tsx | 4 +- .../Storage/StorageExplorer/FileExplorer.tsx | 12 +- .../StorageExplorer/FileExplorerColumn.tsx | 13 +- .../StorageExplorer/FileExplorerHeader.tsx | 24 +- .../FileExplorerHeaderSelection.tsx | 11 +- .../StorageExplorer/FileExplorerRow.tsx | 15 +- .../FileExplorerRowEditing.tsx | 8 +- .../StorageExplorer/FolderContextMenu.tsx | 11 +- .../StorageExplorer/ItemContextMenu.tsx | 11 +- .../StorageExplorer/MoveItemsModal.tsx | 4 +- .../Storage/StorageExplorer/PreviewPane.tsx | 263 +++++++++--------- .../StorageExplorer/StorageExplorer.tsx | 14 +- .../StorageSettings/CreateCredentialModal.tsx | 7 +- .../Storage/StorageSettings/S3Connection.tsx | 19 +- .../StorageSettings/StorageCredItem.tsx | 7 +- 16 files changed, 226 insertions(+), 214 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx index ea17e28b8eb01..d0acf14b0337f 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -3,7 +3,7 @@ import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { ChevronRight, ChevronsDown, ChevronsUp, Clipboard, Eye, FolderPlus } from 'lucide-react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { @@ -17,18 +17,21 @@ interface ColumnContextMenuProps { id: string } -const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') +export const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { const { columns, selectedItems, setSelectedItems, + setView, setSortBy, setSortByOrder, addNewFolderPlaceholder, } = useStorageExplorerStateSnapshot() - const snap = useStorageExplorerStateSnapshot() + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onSelectCreateFolder = (columnIndex = -1) => { addNewFolderPlaceholder(columnIndex) @@ -80,10 +83,10 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { } arrow={} > - snap.setView(STORAGE_VIEWS.COLUMNS)}> + setView(STORAGE_VIEWS.COLUMNS)}> As columns - snap.setView(STORAGE_VIEWS.LIST)}> + setView(STORAGE_VIEWS.LIST)}> As list @@ -128,5 +131,3 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { ) } - -export default ColumnContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx index 1e56e753cd8fe..b33474ac03d9f 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx @@ -12,7 +12,7 @@ const unitMap = { years: 3600 * 24 * 365, } -const CustomExpiryModal = () => { +export const CustomExpiryModal = () => { const { onCopyUrl } = useCopyUrl() const snap = useStorageExplorerStateSnapshot() const { selectedFileCustomExpiry, setSelectedFileCustomExpiry } = snap @@ -100,5 +100,3 @@ const CustomExpiryModal = () => { ) } - -export default CustomExpiryModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx index 0c1442d0cbe12..2c3f7893e442c 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx @@ -5,10 +5,10 @@ import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import ColumnContextMenu from './ColumnContextMenu' -import FileExplorerColumn from './FileExplorerColumn' -import FolderContextMenu from './FolderContextMenu' -import ItemContextMenu from './ItemContextMenu' +import { ColumnContextMenu } from './ColumnContextMenu' +import { FileExplorerColumn } from './FileExplorerColumn' +import { FolderContextMenu } from './FolderContextMenu' +import { ItemContextMenu } from './ItemContextMenu' export interface FileExplorerProps { columns: StorageColumn[] @@ -20,7 +20,7 @@ export interface FileExplorerProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorer = ({ +export const FileExplorer = ({ columns = [], selectedItems = [], itemSearchString, @@ -90,5 +90,3 @@ const FileExplorer = ({ ) } - -export default FileExplorer diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx index 5d1ed5f3066cd..e65e425cdb416 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import InfiniteList from 'components/ui/InfiniteList' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -20,7 +20,7 @@ import { STORAGE_VIEWS, } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRow from './FileExplorerRow' +import { FileExplorerRow } from './FileExplorerRow' const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => { return ( @@ -68,7 +68,7 @@ export interface FileExplorerColumnProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorerColumn = ({ +export const FileExplorerColumn = ({ index = 0, column, fullWidth = false, @@ -83,7 +83,10 @@ const FileExplorerColumn = ({ const fileExplorerColumnRef = useRef(null) const snap = useStorageExplorerStateSnapshot() - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) useEffect(() => { if (fileExplorerColumnRef) { @@ -275,5 +278,3 @@ const FileExplorerColumn = ({ ) } - -export default FileExplorerColumn diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx index 915edffb0c11b..d9a672fd68c5e 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -1,11 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { compact, debounce, isEqual, noop } from 'lodash' -import { useCallback, useEffect, useRef, useState } from 'react' - -import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import APIDocsButton from 'components/ui/APIDocsButton' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Check, ChevronLeft, @@ -20,6 +14,12 @@ import { Upload, X, } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' + +import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import APIDocsButton from 'components/ui/APIDocsButton' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button, @@ -137,10 +137,10 @@ const HeaderBreadcrumbs = ({ interface FileExplorerHeader { itemSearchString: string setItemSearchString: (value: string) => void - onFilesUpload: (event: any, columnIndex: number) => void + onFilesUpload: (event: any, columnIndex?: number) => void } -const FileExplorerHeader = ({ +export const FileExplorerHeader = ({ itemSearchString = '', setItemSearchString = noop, onFilesUpload = noop, @@ -179,7 +179,10 @@ const FileExplorerHeader = ({ const breadcrumbs = columns.map((column) => column.name) const backDisabled = columns.length <= 1 - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) useEffect(() => { if (itemSearchString) setSearchString(itemSearchString) @@ -423,7 +426,6 @@ const FileExplorerHeader = ({
- {/* @ts-ignore */}
) } - -export default FileExplorerHeader diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index 5dea522960d90..9b4559c5b5b71 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -3,14 +3,17 @@ import { Download, Move, Trash2, X } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button } from 'ui' import { downloadFile } from './StorageExplorer.utils' -const FileExplorerHeaderSelection = () => { +export const FileExplorerHeaderSelection = () => { const { ref: projectRef, bucketId } = useParams() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const { selectedItems, @@ -80,5 +83,3 @@ const FileExplorerHeaderSelection = () => {
) } - -export default FileExplorerHeaderSelection diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index 8979041793e68..df4a5eaa8ef1d 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -19,7 +19,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import type { ItemRenderer } from 'components/ui/InfiniteList' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -47,7 +47,7 @@ import { URL_EXPIRY_DURATION, } from '../Storage.constants' import { StorageItem, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRowEditing from './FileExplorerRowEditing' +import { FileExplorerRowEditing } from './FileExplorerRowEditing' import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' @@ -98,13 +98,13 @@ export const RowIcon = ({ return } -export interface FileExplorerRowProps { +interface FileExplorerRowProps { view: STORAGE_VIEWS columnIndex: number selectedItems: StorageItemWithColumn[] } -const FileExplorerRow: ItemRenderer = ({ +export const FileExplorerRow: ItemRenderer = ({ index: itemIndex, item, view = STORAGE_VIEWS.COLUMNS, @@ -139,7 +139,10 @@ const FileExplorerRow: ItemRenderer = ({ const isOpened = openedFolders.length > columnIndex ? openedFolders[columnIndex].name === item.name : false const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id) - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onSelectFile = async (columnIndex: number, file: StorageItem) => { popColumnAtIndex(columnIndex) @@ -455,5 +458,3 @@ const FileExplorerRow: ItemRenderer = ({
) } - -export default FileExplorerRow diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx index 415e2b89cdb39..2ee1a7f02c31e 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -12,7 +12,11 @@ export interface FileExplorerRowEditingProps { columnIndex: number } -const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEditingProps) => { +export const FileExplorerRowEditing = ({ + item, + view, + columnIndex, +}: FileExplorerRowEditingProps) => { const { renameFile, renameFolder, addNewFolder, updateRowStatus } = useStorageExplorerStateSnapshot() @@ -112,5 +116,3 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit ) } - -export default FileExplorerRowEditing diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx index d4b2dafff541c..1242a841a7b62 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx @@ -3,7 +3,7 @@ import { Clipboard, Download, Edit, Trash2 } from 'lucide-react' import { Item, Menu, Separator } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { copyPathToFolder } from './StorageExplorer.utils' @@ -11,10 +11,13 @@ interface FolderContextMenuProps { id: string } -const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { +export const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { const { openedFolders, downloadFolder, setSelectedItemToRename, setSelectedItemsToDelete } = useStorageExplorerStateSnapshot() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) return ( @@ -42,5 +45,3 @@ const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { ) } - -export default FolderContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx index 4ce816e86aacb..0712e4b6578c7 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx @@ -4,7 +4,7 @@ import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' import { useParams } from 'common' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItemWithColumn } from '../Storage.types' @@ -15,7 +15,7 @@ interface ItemContextMenuProps { id: string } -const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { +export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { const { ref: projectRef, bucketId } = useParams() const snap = useStorageExplorerStateSnapshot() const { setSelectedFileCustomExpiry } = snap @@ -28,7 +28,10 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() const isPublic = selectedBucket.public - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onHandleClick = async (event: any, item: StorageItemWithColumn, expiresIn?: number) => { if (item.isCorrupted) return @@ -106,5 +109,3 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { ) } - -export default ItemContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx index 9ec26ea001e21..8da76072e4235 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx @@ -12,7 +12,7 @@ interface MoveItemsModalProps { onSelectMove: (path: string) => void } -const MoveItemsModal = ({ +export const MoveItemsModal = ({ bucketName = '', visible = false, selectedItemsToMove = [], @@ -87,5 +87,3 @@ const MoveItemsModal = ({ ) } - -export default MoveItemsModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index a7f39873b676f..44adc6c0b305a 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -6,7 +6,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -115,7 +115,7 @@ const PreviewFile = ({ item }: { item: StorageItem }) => { ) } -const PreviewPane = () => { +export const PreviewPane = () => { const { ref: projectRef, bucketId } = useParams() const { @@ -127,7 +127,10 @@ const PreviewPane = () => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) if (!file) return null @@ -139,148 +142,144 @@ const PreviewPane = () => { const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown' return ( - <> - -
- {/* Preview Header */} -
- setSelectedFilePreview(undefined)} - /> -
+ +
+ {/* Preview Header */} +
+ setSelectedFilePreview(undefined)} + /> +
- {/* Preview Thumbnail*/} -
-
- -
+ {/* Preview Thumbnail*/} +
+
+
+
-
- {/* Preview Information */} -
-
{file.name}
- {file.isCorrupted && ( -
- -

- File is corrupted, please delete and reupload this file again -

-
- )} - {mimeType && ( +
+ {/* Preview Information */} +
+
{file.name}
+ {file.isCorrupted && ( +
+

- {mimeType} - {size && - {size}} + File is corrupted, please delete and reupload this file again

- )} -
- - {/* Preview Metadata */} -
-
- -

{createdAt}

-
-
- -

{updatedAt}

+ )} + {mimeType && ( +

+ {mimeType} + {size && - {size}} +

+ )} +
+ + {/* Preview Metadata */} +
+
+ +

{createdAt}

+
+
+ +

{updatedAt}

+
- {/* Actions */} -
+ {/* Actions */} +
+ + {selectedBucket.public ? ( - {selectedBucket.public ? ( - - ) : ( - - - - - - onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} - > - Expire in 1 week - - onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} - > - Expire in 1 month - - onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} - > - Expire in 1 year - - setSelectedFileCustomExpiry(file)} - > - Custom expiry - - - - )} -
- } - onClick={() => setSelectedItemsToDelete([file])} - tooltip={{ - content: { - side: 'bottom', - text: !canUpdateFiles - ? 'You need additional permissions to delete this file' - : undefined, - }, - }} - > - Delete file - + ) : ( + + + + + + onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} + > + Expire in 1 week + + onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} + > + Expire in 1 month + + onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} + > + Expire in 1 year + + setSelectedFileCustomExpiry(file)} + > + Custom expiry + + + + )}
+ } + onClick={() => setSelectedItemsToDelete([file])} + tooltip={{ + content: { + side: 'bottom', + text: !canUpdateFiles + ? 'You need additional permissions to delete this file' + : undefined, + }, + }} + > + Delete file +
- - +
+ ) } - -export default PreviewPane diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index e352dc2b9f7a7..d3616e59a6550 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -8,12 +8,12 @@ import { IS_PLATFORM } from 'lib/constants' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import { ConfirmDeleteModal } from './ConfirmDeleteModal' -import CustomExpiryModal from './CustomExpiryModal' -import FileExplorer from './FileExplorer' -import FileExplorerHeader from './FileExplorerHeader' -import FileExplorerHeaderSelection from './FileExplorerHeaderSelection' -import MoveItemsModal from './MoveItemsModal' -import PreviewPane from './PreviewPane' +import { CustomExpiryModal } from './CustomExpiryModal' +import { FileExplorer } from './FileExplorer' +import { FileExplorerHeader } from './FileExplorerHeader' +import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection' +import { MoveItemsModal } from './MoveItemsModal' +import { PreviewPane } from './PreviewPane' interface StorageExplorerProps { bucket: Bucket @@ -126,7 +126,7 @@ export const StorageExplorer = ({ bucket }: StorageExplorerProps) => { /** File manipulation methods */ - const onFilesUpload = async (event: any, columnIndex = -1) => { + const onFilesUpload = async (event: any, columnIndex: number = -1) => { event.persist() const items = event.target.files || event.dataTransfer.items const isDrop = !isEmpty(get(event, ['dataTransfer', 'items'], [])) diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx index 8d331ac886dec..cb5b6de2ab9b8 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Dialog, @@ -39,7 +39,10 @@ export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentia const isProjectActive = useIsProjectActive() const [showSuccess, setShowSuccess] = useState(false) - const canCreateCredentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canCreateCredentials } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: config } = useProjectStorageConfigQuery({ projectRef }) const isS3ConnectionEnabled = config?.features.s3Protocol.enabled diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index 85085e00e88f7..0057b4e751d4b 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -22,7 +22,7 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, @@ -54,8 +54,12 @@ export const S3Connection = () => { const [openDeleteDialog, setOpenDeleteDialog] = useState(false) const [deleteCred, setDeleteCred] = useState<{ id: string; description: string }>() - const canReadS3Credentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') - const canUpdateStorageSettings = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canReadS3Credentials, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') + const { can: canUpdateStorageSettings } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { @@ -170,7 +174,7 @@ export const S3Connection = () => {
- {!canUpdateStorageSettings && ( + {!isLoadingPermissions && !canUpdateStorageSettings && (

You need additional permissions to update storage settings @@ -216,6 +220,7 @@ export const S3Connection = () => { +

@@ -227,10 +232,10 @@ export const S3Connection = () => {
- {!canReadS3Credentials ? ( - - ) : projectIsLoading ? ( + {projectIsLoading || isLoadingPermissions ? ( + ) : !canReadS3Credentials ? ( + ) : !isProjectActive ? ( diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx index d1dd01b9b1bf1..7eb0717b6ff70 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx @@ -3,7 +3,7 @@ import { differenceInDays } from 'date-fns' import { MoreVertical, TrashIcon } from 'lucide-react' import CopyButton from 'components/ui/CopyButton' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -25,7 +25,10 @@ export const StorageCredItem = ({ access_key: string onDeleteClick: (id: string) => void }) => { - const canRemoveAccessKey = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canRemoveAccessKey } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) function daysSince(date: string) { const now = new Date() From f066c02c691b297cf8afe5dac0eebfdfaab04771 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 8 Sep 2025 14:15:39 +0800 Subject: [PATCH 4/9] Shift refresh button to header in table editor and make realtime + API docs button smaller (#38463) * Shift refresh button to header in table editor and make realtime button + api docs button smaller * Fix ts --- apps/studio/components/grid/SupabaseGrid.tsx | 8 +-- .../grid/components/footer/Footer.tsx | 13 +---- .../grid/components/header/Header.tsx | 9 ++-- .../grid/components/header/RefreshButton.tsx | 22 ++++---- .../interfaces/Auth/Users/UsersV2.tsx | 2 +- .../StorageExplorer/FileExplorerHeader.tsx | 2 +- .../TableGridEditor/GridHeaderActions.tsx | 50 ++++++++++++------- .../ForeignRowSelector/ForeignRowSelector.tsx | 2 +- .../TableGridEditor/TableDefinition.tsx | 6 +-- .../TableGridEditor/TableGridEditor.tsx | 2 +- .../EdgeFunctionDetailsLayout.tsx | 2 +- apps/studio/components/ui/APIDocsButton.tsx | 23 +++++---- 12 files changed, 73 insertions(+), 68 deletions(-) diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 3129c172d0e66..3bf86dd9ee2fe 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -13,9 +13,9 @@ import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Shortcuts } from './components/common/Shortcuts' -import Footer from './components/footer/Footer' +import { Footer } from './components/footer/Footer' import { Grid } from './components/grid/Grid' -import Header, { HeaderProps } from './components/header/Header' +import { Header, HeaderProps } from './components/header/Header' import { RowContextMenu } from './components/menu' import { GridProps } from './types' @@ -84,7 +84,7 @@ export const SupabaseGrid = ({ return (
-
+
{children || ( <> @@ -99,7 +99,7 @@ export const SupabaseGrid = ({ filters={filters} onApplyFilters={onApplyFilters} /> -
+
)} diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index f4680d0acacb2..a6281989b9c03 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -5,14 +5,9 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike, isViewLike } from 'data/table-editor/table-editor-types' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' -import RefreshButton from '../header/RefreshButton' import { Pagination } from './pagination' -export interface FooterProps { - isRefetching?: boolean -} - -const Footer = ({ isRefetching }: FooterProps) => { +export const Footer = () => { const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: project } = useSelectedProjectQuery() @@ -41,10 +36,6 @@ const Footer = ({ isRefetching }: FooterProps) => { {selectedView === 'data' && }
- {entity && selectedView === 'data' && ( - - )} - {(isViewSelected || isTableSelected) && ( { ) } - -export default Footer diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index fde164a247cc5..821a359c06d11 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' import { useTableSort } from 'components/grid/hooks/useTableSort' -import GridHeaderActions from 'components/interfaces/TableGridEditor/GridHeaderActions' +import { GridHeaderActions } from 'components/interfaces/TableGridEditor/GridHeaderActions' import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' @@ -56,9 +56,10 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = ( export type HeaderProps = { customHeader: ReactNode + isRefetching: boolean } -const Header = ({ customHeader }: HeaderProps) => { +export const Header = ({ customHeader, isRefetching }: HeaderProps) => { const snap = useTableEditorTableStateSnapshot() return ( @@ -71,14 +72,12 @@ const Header = ({ customHeader }: HeaderProps) => { ) : ( )} - +
) } -export default Header - const DefaultHeader = () => { const { ref: projectRef } = useParams() const { data: org } = useSelectedOrganizationQuery() diff --git a/apps/studio/components/grid/components/header/RefreshButton.tsx b/apps/studio/components/grid/components/header/RefreshButton.tsx index 1020e419b0235..91c611c8c9ad0 100644 --- a/apps/studio/components/grid/components/header/RefreshButton.tsx +++ b/apps/studio/components/grid/components/header/RefreshButton.tsx @@ -2,15 +2,15 @@ import { useQueryClient } from '@tanstack/react-query' import { RefreshCw } from 'lucide-react' import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { tableRowKeys } from 'data/table-rows/keys' -import { Button } from 'ui' export type RefreshButtonProps = { tableId?: number isRefetching?: boolean } -const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { +export const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { const { ref } = useParams() const queryClient = useQueryClient() const queryKey = tableRowKeys.tableRowsAndCount(ref, tableId) @@ -20,14 +20,18 @@ const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { } return ( - + className="w-7 h-7 p-0" + tooltip={{ + content: { + side: 'bottom', + text: 'Refresh table data', + }, + }} + /> ) } -export default RefreshButton diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 0ead1d0a20090..49c683f70b236 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import AlertError from 'components/ui/AlertError' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' import { FormHeader } from 'components/ui/Forms/FormHeader' diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx index d9a672fd68c5e..0d080f6c90f6b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -17,7 +17,7 @@ import { import { useCallback, useEffect, useRef, useState } from 'react' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index c4dab21d894c7..0322cba739a24 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -5,8 +5,9 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' @@ -45,9 +46,10 @@ import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal' export interface GridHeaderActionsProps { table: Entity + isRefetching: boolean } -const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { +export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() @@ -277,13 +279,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { -

+

Row Level Security (RLS) -

-
+ +

You can restrict and control who can read, write and update data in this table using Row Level Security. @@ -293,14 +295,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { table.

{!isSchemaLocked && ( -
- -
+ )}
@@ -443,8 +444,9 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { {isTable && realtimeEnabled && ( - + {!isRealtimeEnabled && 'Enable Realtime'} + )} + {doesHaveAutoGeneratedAPIDocs && } + +
)} {
) } - -export default GridHeaderActions diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx index 9a2a6ac1c87df..e815f7c3873f8 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -4,7 +4,7 @@ import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { useParams } from 'common' -import RefreshButton from 'components/grid/components/header/RefreshButton' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive' import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive' import type { Filter, Sort } from 'components/grid/types' diff --git a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx index 45e81d6abf84e..f424dc65ba47c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { useMemo, useRef } from 'react' import { useParams } from 'common' -import Footer from 'components/grid/components/footer/Footer' +import { Footer } from 'components/grid/components/footer/Footer' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableDefinitionQuery } from 'data/database/table-definition-query' import { useViewDefinitionQuery } from 'data/database/view-definition-query' @@ -24,7 +24,7 @@ export interface TableDefinitionProps { entity?: Entity } -const TableDefinition = ({ entity }: TableDefinitionProps) => { +export const TableDefinition = ({ entity }: TableDefinitionProps) => { const { ref } = useParams() const editorRef = useRef(null) const monacoRef = useRef(null) @@ -132,5 +132,3 @@ const TableDefinition = ({ entity }: TableDefinitionProps) => { ) } - -export default TableDefinition diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index d1bff23294c1b..f0c1904650a5f 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -22,7 +22,7 @@ import { Button } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import DeleteConfirmationDialogs from './DeleteConfirmationDialogs' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' -import TableDefinition from './TableDefinition' +import { TableDefinition } from './TableDefinition' export interface TableGridEditorProps { isLoadingSelectedTable?: boolean diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index ae035f651381e..ef94be5b6d26e 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query' diff --git a/apps/studio/components/ui/APIDocsButton.tsx b/apps/studio/components/ui/APIDocsButton.tsx index dcc0a5b2b3a89..4a2f4971e49b3 100644 --- a/apps/studio/components/ui/APIDocsButton.tsx +++ b/apps/studio/components/ui/APIDocsButton.tsx @@ -1,27 +1,30 @@ -import { Code } from 'lucide-react' +import { BookOpenText } from 'lucide-react' import { useAppStateSnapshot } from 'state/app-state' -import { Button } from 'ui' +import { ButtonTooltip } from './ButtonTooltip' interface APIDocsButtonProps { section?: string[] } -const APIDocsButton = ({ section }: APIDocsButtonProps) => { +export const APIDocsButton = ({ section }: APIDocsButtonProps) => { const snap = useAppStateSnapshot() return ( - + icon={} + className="h-7 w-7" + tooltip={{ + content: { + side: 'bottom', + text: 'API Docs', + }, + }} + /> ) } - -export default APIDocsButton From 82dcee3788a1b44cc66b42a62e0a5b82ed7decbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Mon, 8 Sep 2025 14:26:03 +0800 Subject: [PATCH 5/9] feat: ensure customer puts down address on upgrade (#38465) For customers who have not yet put down a billing address, we want to ensure that they put down an address when they upgrade their plan. Customers can also no longer unset their billing address. --- .../BillingCustomerDataForm.tsx | 45 +++++-------------- .../Subscription/PaymentMethodSelection.tsx | 12 ++--- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx index 98e84f71649c2..7440ff506fb57 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -32,39 +32,18 @@ interface BillingCustomerDataFormProps { } // Define the expected form values structure and validation schema -export const BillingCustomerDataSchema = z - .object({ - billing_name: z.string().min(3, 'Name must be at least 3 letters long'), - line1: z.string().optional(), - line2: z.string().optional(), - city: z.string().optional(), - state: z.string().optional(), - postal_code: z.string().optional(), - country: z.string().optional(), - tax_id_type: z.string(), - tax_id_value: z.string(), - tax_id_name: z.string(), - }) - .refine( - (data) => { - // its fine to just set the name, but once any other field is set, requires full address - const hasAnyField = data.line1 || data.line2 || data.city || data.state || data.postal_code - // If any field has value, country and line1 must have values. - return !hasAnyField || (!!data.country && !!data.line1) - }, - { - message: 'Country and Address line 1 are required if any other field is provided.', - path: ['line1'], - } - ) - .refine((data) => !(!!data.line1 && !data.country), { - message: 'Please select a country', - path: ['country'], - }) - .refine((data) => !(!!data.country && !data.line1), { - message: 'Please provide an address line 1', - path: ['line1'], - }) +export const BillingCustomerDataSchema = z.object({ + billing_name: z.string().min(3, 'Name must be at least 3 letters long'), + line1: z.string().trim().min(3, 'Address line 1 is required'), + line2: z.string().optional(), + city: z.string().trim().min(2, 'City is required'), + state: z.string().trim(), + postal_code: z.string().trim().min(1, 'Postal code is required'), + country: z.string().trim().min(1, 'Country is required'), + tax_id_type: z.string(), + tax_id_value: z.string(), + tax_id_name: z.string(), +}) export type BillingCustomerDataFormValues = z.infer diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 114ed71e19f01..8f60d2a372f3b 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -63,8 +63,6 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( }) const { data: taxId, isLoading: isCustomerTaxIdLoading } = useOrganizationTaxIdQuery({ slug }) - const hidePaymentMethodsWithoutAddress = useFlag('hidePaymentMethodsWithoutAddress') - const { data: allPaymentMethods, isLoading } = useOrganizationPaymentMethodsQuery({ slug }) const paymentMethods = useMemo(() => { @@ -74,18 +72,16 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( defaultPaymentMethodId: null, } - const filtered = allPaymentMethods.data.filter( - (pm) => !hidePaymentMethodsWithoutAddress || pm.has_address - ) return { - data: filtered, + // force customer to put down address via payment method creation flow if they don't have an address set + data: customerProfile?.address == null ? [] : allPaymentMethods.data, defaultPaymentMethodId: allPaymentMethods.data.some( (pm) => pm.id === allPaymentMethods.defaultPaymentMethodId ) ? allPaymentMethods.defaultPaymentMethodId : null, } - }, [allPaymentMethods]) + }, [allPaymentMethods, customerProfile]) const captchaRefCallback = useCallback((node: any) => { setCaptchaRef(node) @@ -223,7 +219,7 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( />
- {isLoading ? ( + {isLoading || isCustomerProfileLoading ? (

Retrieving payment methods

From 72db84b0b08fbd658c03aefa3f9513bf3ec178e7 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:04:50 +1000 Subject: [PATCH 6/9] fix: remove duplicate tweet (#38500) remove duplicate tweet --- packages/shared-data/tweets.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/shared-data/tweets.ts b/packages/shared-data/tweets.ts index fddb5b92e4232..cba89c1a39bf0 100644 --- a/packages/shared-data/tweets.ts +++ b/packages/shared-data/tweets.ts @@ -77,12 +77,6 @@ const tweets = [ handle: 'viratt_mankali', img_url: '/images/twitter-profiles/GtrVV2dD_400x400.jpg', }, - { - text: "Working with @supabase has been one of the best dev experiences I've had lately. Incredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition. I definitely plan to use it on any and all future projects.", - url: 'https://twitter.com/thatguy_tex/status/1497602628410388480', - handle: 'thatguy_tex', - img_url: '/images/twitter-profiles/09HouOSt_400x400.jpg', - }, { text: '@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend. @IngoKpp now I see your joy with Supabase #coding #fullstackwebdev', url: 'https://twitter.com/IxoyeDesign/status/1497473731777728512', From fa62afb09765be473c698d8b5561b202db28f7cb Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Mon, 8 Sep 2025 16:58:54 +0800 Subject: [PATCH 7/9] fix: do not auto submit on toggling data branch (#38479) * fix: display badge instead of tooltip * Clean up --------- Co-authored-by: Joshen Lim --- .../BranchManagement/CreateBranchModal.tsx | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 9ffa0387be786..9fb2103792bf8 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -46,9 +46,6 @@ import { Input_Shadcn_, Label_Shadcn_ as Label, Switch, - Tooltip, - TooltipContent, - TooltipTrigger, cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -92,7 +89,11 @@ export const CreateBranchModal = () => { useCheckGithubBranchValidity({ onError: () => {}, }) - const { data: cloneBackups, error: cloneBackupsError } = useCloneBackupsQuery( + const { + data: cloneBackups, + error: cloneBackupsError, + isLoading: isLoadingCloneBackups, + } = useCloneBackupsQuery( { projectRef }, { // [Joshen] Only trigger this request when the modal is opened @@ -359,26 +360,29 @@ export const CreateBranchModal = () => { name="withData" render={({ field }) => ( + + {!disableBackupsCheck && (isLoadingCloneBackups || noPhysicalBackups) && ( + + Requires PITR + + )} + + } layout="flex-row-reverse" + className="[&>div>label]:mb-1" description="Clone production data into this branch" > - - - - - - - {!disableBackupsCheck && noPhysicalBackups && ( - - PITR is required for the project to clone data into the branch - - )} - + + + )} /> From fa4510dae74e50cbefb07f19632bf5e17911609a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 8 Sep 2025 17:29:43 +0800 Subject: [PATCH 8/9] Update RestoreFailedState and PauseFailedState to include link to database backups upfront (#38510) * Update RestoreFailedState and PauseFailedState to include link to database backups upfront * Update tooltip text --- .../ProjectLayout/PauseFailedState.tsx | 30 +++++++++++----- .../layouts/ProjectLayout/ProjectLayout.tsx | 4 +-- .../ProjectLayout/RestoreFailedState.tsx | 34 +++++++++++++------ 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx index 0ba928c7ed520..d82d07658b797 100644 --- a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx @@ -7,20 +7,25 @@ import { useParams } from 'common' import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' +import { InlineLink } from 'components/ui/InlineLink' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useDownloadableBackupQuery } from 'data/database/backup-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, CriticalIcon, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' -const PauseFailedState = () => { +export const PauseFailedState = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [visible, setVisible] = useState(false) - const canDeleteProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canDeleteProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { project_id: project?.id }, + } + ) const { data } = useDownloadableBackupQuery({ projectRef: ref }) const backups = data?.backups ?? [] @@ -57,7 +62,11 @@ const PauseFailedState = () => {

Something went wrong while pausing your project

Your project's data is intact, but your project is inaccessible due to the failure - while pausing. Please contact support for assistance. + while pausing. Database backups for this project can still be accessed{' '} + here. +

+

+ Please contact support for assistance.

@@ -78,7 +87,12 @@ const PauseFailedState = () => { tooltip={{ content: { side: 'bottom', - text: backups.length === 0 ? 'No available backups to download' : undefined, + text: + data?.status === 'physical-backups-enabled' + ? 'No available backups to download as project is on physical backups' + : backups.length === 0 + ? 'No available backups to download' + : undefined, }, }} onClick={onClickDownloadBackup} @@ -123,5 +137,3 @@ const PauseFailedState = () => { ) } - -export default PauseFailedState diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 47e0c76f2b90b..90cd9ca68929d 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -24,12 +24,12 @@ import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import { LoadingState } from './LoadingState' import { ProjectPausedState } from './PausedState/ProjectPausedState' -import PauseFailedState from './PauseFailedState' +import { PauseFailedState } from './PauseFailedState' import PausingState from './PausingState' import ProductMenuBar from './ProductMenuBar' import { ResizingState } from './ResizingState' import RestartingState from './RestartingState' -import RestoreFailedState from './RestoreFailedState' +import { RestoreFailedState } from './RestoreFailedState' import RestoringState from './RestoringState' import { UpgradingState } from './UpgradingState' diff --git a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx index 9120f3b2f4d38..edb62abd66c45 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx @@ -7,20 +7,23 @@ import { useParams } from 'common' import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' +import { InlineLink } from 'components/ui/InlineLink' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useDownloadableBackupQuery } from 'data/database/backup-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, CriticalIcon, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' -const RestoreFailedState = () => { +export const RestoreFailedState = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [visible, setVisible] = useState(false) - const canDeleteProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canDeleteProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { resource: { project_id: project?.id } } + ) const { data } = useDownloadableBackupQuery({ projectRef: ref }) const backups = data?.backups ?? [] @@ -56,8 +59,12 @@ const RestoreFailedState = () => {

Something went wrong while restoring your project

- Your project's data is intact, but your project is inaccessible due to the - restoration failure. Please contact support for assistance. + Your project's data is intact, but your project is inaccessible due to a + restoration failure. Database backups for this project can still be accessed{' '} + here. +

+

+ Please contact support for assistance.

@@ -70,6 +77,7 @@ const RestoreFailedState = () => { Contact support + } @@ -78,16 +86,22 @@ const RestoreFailedState = () => { tooltip={{ content: { side: 'bottom', - text: backups.length === 0 ? 'No available backups to download' : undefined, + text: + data?.status === 'physical-backups-enabled' + ? 'No available backups to download as project is on physical backups' + : backups.length === 0 + ? 'No available backups to download' + : undefined, }, }} onClick={onClickDownloadBackup} > Download backup + -