diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 73167045ed609..5efc952762226 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -27,9 +27,9 @@ import { getGeneralPolicyTemplates } from './PolicyEditorModal.constants' import PolicyEditorModalTitle from './PolicyEditorModalTitle' interface PolicyEditorModalProps { - visible: boolean - schema: string - table: string + visible?: boolean + schema?: string + table?: string selectedPolicyToEdit: any showAssistantPreview?: boolean onSelectCancel: () => void diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx index 5e273759dce62..bdb31dd0d3d39 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx @@ -4,10 +4,10 @@ import { useState } from 'react' import { Badge, HoverCard, HoverCardContent, HoverCardTrigger, Input, cn } from 'ui' import { Markdown } from 'components/interfaces/Markdown' -import { SimpleCodeBlock } from 'ui' import CardButton from 'components/ui/CardButton' import CopyButton from 'components/ui/CopyButton' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' +import { SimpleCodeBlock } from 'ui' import { getGeneralPolicyTemplates, getQueuePolicyTemplates, diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx index 08ec649e6d238..b201b5b1b77cb 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react' import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import InformationBox from 'components/ui/InformationBox' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx index 56dbdfbd0130e..5c19e17b08e5f 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx index 27955f326a1f0..59d71ef77bf46 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import InformationBox from 'components/ui/InformationBox' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index f543e5469dc26..b1cdf2a8e9a67 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -4,9 +4,9 @@ import Link from 'next/link' import { useMemo, useState } from 'react' import { useParams } from 'common' -import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useTablesQuery } from 'data/tables/tables-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -94,7 +94,7 @@ export const PublicationsTables = () => { {isSuccess && (tables.length === 0 ? ( - + setFilterString('')} /> ) : ( diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index 0c6fe60f15b80..b33a2d1ce47d2 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -4,21 +4,21 @@ import { Plus, Search, X } from 'lucide-react' import { parseAsBoolean, useQueryState } from 'nuqs' import { useRef, useState } from 'react' +import type { PostgresRole } from '@supabase/postgres-meta' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import SparkBar from 'components/ui/SparkBar' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Badge, Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { CreateRolePanel } from './CreateRolePanel' import { DeleteRoleModal } from './DeleteRoleModal' import { RoleRow } from './RoleRow' import { RoleRowSkeleton } from './RoleRowSkeleton' import { SUPABASE_ROLES } from './Roles.constants' -import type { PostgresRole } from '@supabase/postgres-meta' type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number] diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx index a4e9c23bca2e4..27b48c39b1d5a 100644 --- a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -6,10 +6,10 @@ import { useState } from 'react' import { PostgresColumn } from '@supabase/postgres-meta' import { useParams } from 'common' -import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' @@ -120,7 +120,10 @@ export const ColumnList = ({ {isSuccess && ( <> {columns.length === 0 ? ( - + setFilterString('')} + /> ) : (
))} - - {currentPlanMeta.id === 'free' && selectedTier !== 'tier_free' && ( -
- -

- Please note: Existing support cases will remain in the Free support - queue after your subscription is upgraded. For faster assistance under - your new plan, please open a new support case once the upgrade is - complete. -

-
-
- )} )} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx index 8c1b10def657f..948b5cf63414f 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx @@ -11,7 +11,6 @@ import { convertKVStringArrayToJson, formatWrapperTables, } from 'components/interfaces/Integrations/Wrappers/Wrappers.utils' -import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket' import { ScaffoldContainer, ScaffoldSection, @@ -25,7 +24,6 @@ import { } from 'data/database-extensions/database-extensions-query' import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query' import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation' -import { AnalyticsBucket } from 'data/storage/analytics-buckets-query' import { useIcebergNamespacesQuery } from 'data/storage/iceberg-namespaces-query' import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -34,6 +32,7 @@ import { Button, Card, CardContent } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { GenericTableLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteAnalyticsBucketModal } from '../DeleteAnalyticsBucketModal' +import { useSelectedAnalyticsBucket } from '../useSelectedAnalyticsBucket' import { BucketHeader } from './BucketHeader' import { ConnectTablesDialog } from './ConnectTablesDialog' import { NamespaceWithTables } from './NamespaceWithTables' @@ -48,12 +47,11 @@ export const AnalyticBucketDetails = () => { const { data: project } = useSelectedProjectQuery() const { state: extensionState } = useIcebergWrapperExtension() const { - bucket: _bucket, + data: bucket, error: bucketError, isSuccess: isSuccessBucket, isError: isErrorBucket, - } = useSelectedBucket() - const bucket = _bucket as undefined | AnalyticsBucket + } = useSelectedAnalyticsBucket() const [modal, setModal] = useState<'delete' | null>(null) // [Joshen] Namespaces are now created asynchronously when the pipeline is started, so long poll after @@ -332,7 +330,7 @@ export const AnalyticBucketDetails = () => { diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/useSelectedAnalyticsBucket.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/useSelectedAnalyticsBucket.ts new file mode 100644 index 0000000000000..510f7a636bee9 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/useSelectedAnalyticsBucket.ts @@ -0,0 +1,18 @@ +import { useParams } from 'common' +import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query' +import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query' + +export const useSelectedAnalyticsBucket = () => { + const { ref, bucketId } = useParams() + const hasIcebergEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref }) + + return useAnalyticsBucketsQuery( + { projectRef: ref }, + { + enabled: hasIcebergEnabled, + select(data) { + return data.find((x) => x.id === bucketId) + }, + } + ) +} diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 11c3ebbf41166..a9b8eb011bb22 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -214,6 +214,7 @@ export const CreateBucketModal = ({ disabled={!canCreateBuckets} style={{ justifyContent: 'start' }} onClick={() => setVisible(true)} + tabIndex={!canCreateBuckets ? -1 : 0} tooltip={{ content: { side: 'bottom', diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx b/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx index dca347a3d510d..2d9dcbb1ebba1 100644 --- a/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx +++ b/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx @@ -10,7 +10,7 @@ import { formatBytes } from 'lib/helpers' import { ChevronRight } from 'lucide-react' import { useRouter } from 'next/navigation' import type React from 'react' -import { Badge, cn, TableCell, TableHead, TableHeader, TableRow } from 'ui' +import { Badge, TableCell, TableHead, TableHeader, TableRow } from 'ui' type BucketTableMode = 'standard' | 'virtualized' @@ -100,7 +100,18 @@ export const BucketTableRow = ({ } return ( - + handleBucketNavigation(bucket.id, event)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + handleBucketNavigation(bucket.id, event) + } + }} + tabIndex={0} + > @@ -109,12 +120,6 @@ export const BucketTableRow = ({

{bucket.id}

{bucket.public && Public} - diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx index 9444eb99a50c7..a9719679b61a7 100644 --- a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx @@ -80,7 +80,7 @@ export const FilesBuckets = () => { /> - diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/useSelectedBucket.ts b/apps/studio/components/interfaces/Storage/FilesBuckets/useSelectedBucket.ts new file mode 100644 index 0000000000000..d78ef9919dac5 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/FilesBuckets/useSelectedBucket.ts @@ -0,0 +1,15 @@ +import { useParams } from 'common' +import { useBucketsQuery } from 'data/storage/buckets-query' + +export const useSelectedBucket = () => { + const { ref, bucketId } = useParams() + + return useBucketsQuery( + { projectRef: ref }, + { + select(data) { + return data.find((b) => b.id === bucketId) + }, + } + ) +} diff --git a/apps/studio/components/interfaces/Storage/Storage.utils.ts b/apps/studio/components/interfaces/Storage/Storage.utils.ts index 5125475065232..7326d6fe6d0a2 100644 --- a/apps/studio/components/interfaces/Storage/Storage.utils.ts +++ b/apps/studio/components/interfaces/Storage/Storage.utils.ts @@ -1,6 +1,8 @@ +import { PostgresPolicy } from '@supabase/postgres-meta' import { difference, groupBy } from 'lodash' import { useRouter } from 'next/router' +import { Bucket } from 'data/storage/buckets-query' import { STORAGE_CLIENT_LIBRARY_MAPPINGS } from './Storage.constants' import type { StoragePolicyFormField } from './Storage.types' @@ -20,8 +22,8 @@ const shortHash = (str: string) => { * Output: [{ bucket: , policies: }] * @param {Array} policies: All policies from a table in a schema */ -export const formatPoliciesForStorage = (buckets: any[], policies: any[]) => { - if (policies.length === 0) return policies +export const formatPoliciesForStorage = (buckets: Bucket[], policies: PostgresPolicy[]) => { + if (policies.length === 0) return [] /** * Format policies from storage objects to: diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts deleted file mode 100644 index b44317600dd22..0000000000000 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useParams } from 'common' -import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query' -import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query' -import { useBucketsQuery } from 'data/storage/buckets-query' -import { useStorageV2Page } from '../Storage.utils' - -export const useSelectedBucket = () => { - const { ref, bucketId } = useParams() - const page = useStorageV2Page() - const hasIcebergEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref }) - - const { - data: analyticsBuckets = [], - isSuccess: isSuccessAnalyticsBuckets, - isError: isErrorAnalyticsBuckets, - error: errorAnalyticsBuckets, - } = useAnalyticsBucketsQuery({ projectRef: ref }) - - const { - data: buckets = [], - isSuccess: isSuccessBuckets, - isError: isErrorBuckets, - error: errorBuckets, - } = useBucketsQuery({ projectRef: ref }) - - const isSuccess = hasIcebergEnabled - ? isSuccessBuckets && isSuccessAnalyticsBuckets - : isSuccessBuckets - const isError = hasIcebergEnabled ? isErrorBuckets || isErrorAnalyticsBuckets : isErrorBuckets - const error = hasIcebergEnabled ? errorBuckets || errorAnalyticsBuckets : errorBuckets - - const bucket = - page === 'files' - ? buckets.find((b) => b.id === bucketId) - : page === 'analytics' - ? analyticsBuckets.find((b: any) => b.id === bucketId) - : buckets.find((b) => b.id === bucketId) - - return { bucket, isSuccess, isError, error } -} diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx index 2df739a44e9f8..9143a92a345b0 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx @@ -1,6 +1,9 @@ +import { PostgresPolicy } from '@supabase/postgres-meta' import { useParams } from 'common' -import { filter, find, get, isEmpty } from 'lodash' -import { useState } from 'react' +import { isEmpty } from 'lodash' +import { Search, X } from 'lucide-react' +import { parseAsString, useQueryState } from 'nuqs' +import { useMemo, useState } from 'react' import { toast } from 'sonner' import PolicyEditorModal from 'components/interfaces/Auth/Policies/PolicyEditorModal' @@ -9,13 +12,16 @@ import { ScaffoldSectionDescription, ScaffoldSectionTitle, } from 'components/layouts/Scaffold' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePolicyCreateMutation } from 'data/database-policies/database-policy-create-mutation' import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation' import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' import { useBucketsQuery } from 'data/storage/buckets-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { Button } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' import { formatPoliciesForStorage } from '../Storage.utils' import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow' @@ -23,18 +29,24 @@ import StoragePoliciesEditPolicyModal from './StoragePoliciesEditPolicyModal' import StoragePoliciesPlaceholder from './StoragePoliciesPlaceholder' export const StoragePolicies = () => { - const { data: project } = useSelectedProjectQuery() const { ref: projectRef } = useParams() + const { data: project } = useSelectedProjectQuery() - const { data, isLoading: isLoadingBuckets } = useBucketsQuery({ projectRef }) - const buckets = data ?? [] + const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState() + const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState() + const [isEditingPolicyForBucket, setIsEditingPolicyForBucket] = useState<{ + bucket: string + table: string + }>() + const [searchString, setSearchString] = useQueryState( + 'search', + parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true }) + ) - const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState({}) - const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState({}) - const [isEditingPolicyForBucket, setIsEditingPolicyForBucket] = useState({}) + const { data: buckets = [], isLoading: isLoadingBuckets } = useBucketsQuery({ projectRef }) const { - data: policiesData, + data: policies = [], refetch, isLoading: isLoadingPolicies, } = useDatabasePoliciesQuery({ @@ -42,7 +54,6 @@ export const StoragePolicies = () => { connectionString: project?.connectionString, schema: 'storage', }) - const policies = policiesData ?? [] const isLoading = isLoadingBuckets || isLoadingPolicies @@ -54,7 +65,7 @@ export const StoragePolicies = () => { onSuccess: async () => { await refetch() toast.success('Successfully deleted policy!') - setSelectedPolicyToDelete({}) + setSelectedPolicyToDelete(undefined) }, }) @@ -62,24 +73,50 @@ export const StoragePolicies = () => { const showStoragePolicyEditor = isEmpty(selectedPolicyToEdit) && !isEmpty(isEditingPolicyForBucket) && - get(isEditingPolicyForBucket, ['bucket'], '').length > 0 + (isEditingPolicyForBucket.bucket ?? '').length > 0 const showGeneralPolicyEditor = !isEmpty(isEditingPolicyForBucket) && !showStoragePolicyEditor // Policies under storage.objects - const storageObjectsPolicies = filter(policies, { table: 'objects' }) - const formattedStorageObjectPolicies = formatPoliciesForStorage(buckets, storageObjectsPolicies) - const ungroupedPolicies = get( - find(formattedStorageObjectPolicies, { name: 'Ungrouped' }), - ['policies'], - [] + const storageObjectsPolicies = policies.filter( + (x) => x.schema === 'storage' && x.table === 'objects' ) + const formattedStorageObjectPolicies = formatPoliciesForStorage(buckets, storageObjectsPolicies) + const ungroupedPolicies = + formattedStorageObjectPolicies.find((x) => x.name === 'Ungrouped')?.policies ?? [] // Policies under storage.buckets - const storageBucketPolicies = filter(policies, { table: 'buckets' }) + const storageBucketPolicies = policies.filter( + (x) => x.schema === 'storage' && x.table === 'buckets' + ) + + /** + * Filter buckets based on search string + * - Filter buckets by name matching the search string + * - Show all policies for filtered buckets (policies are not filtered) + */ + const filteredBucketsWithPolicies = useMemo(() => { + const searchFilter = searchString?.toLowerCase() || '' + + // Filter buckets by name if search filter is present + const filteredBucketsList = searchFilter + ? buckets.filter((bucket) => bucket.name.toLowerCase().includes(searchFilter)) + : buckets + + // Get policies for filtered buckets (show all policies, don't filter them) + // Show all filtered buckets, even if they don't have policies (similar to auth/policies.tsx) + const filteredBucketsWithPoliciesList = filteredBucketsList.map((bucket) => { + const policies = + formattedStorageObjectPolicies.find((x) => x.name === bucket.name)?.policies ?? [] + return { bucket, policies } + }) + + // Schema-level policies should always be shown, unaffected by search filter + return filteredBucketsWithPoliciesList + }, [buckets, searchString, formattedStorageObjectPolicies]) const onSelectPolicyAdd = (bucketName = '', table = '') => { - setSelectedPolicyToEdit({}) + setSelectedPolicyToEdit(undefined) setIsEditingPolicyForBucket({ bucket: bucketName, table }) } @@ -89,11 +126,11 @@ export const StoragePolicies = () => { } const onCancelPolicyEdit = () => { - setIsEditingPolicyForBucket({}) + setIsEditingPolicyForBucket(undefined) } const onSelectPolicyDelete = (policy: any) => setSelectedPolicyToDelete(policy) - const onCancelPolicyDelete = () => setSelectedPolicyToDelete({}) + const onCancelPolicyDelete = () => setSelectedPolicyToDelete(undefined) const onSavePolicySuccess = async () => { toast.success('Successfully saved policy!') @@ -155,6 +192,10 @@ export const StoragePolicies = () => { console.error('Project is required') return true } + if (!selectedPolicyToEdit) { + console.error('Unable to find policy') + return true + } try { await updateDatabasePolicy({ @@ -172,6 +213,8 @@ export const StoragePolicies = () => { const onDeletePolicy = async () => { if (!project) return console.error('Project is required') + if (!selectedPolicyToDelete) return console.error('Unable to find policy') + deleteDatabasePolicy({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -180,7 +223,7 @@ export const StoragePolicies = () => { } return ( -
+ <> {isLoading ? ( @@ -194,22 +237,51 @@ export const StoragePolicies = () => { {buckets.length === 0 && } + {buckets.length > 0 && ( +
+ { + const str = e.target.value + setSearchString(str) + }} + icon={} + actions={ + searchString ? ( +
+ )} + + {searchString.length > 0 && filteredBucketsWithPolicies.length === 0 && ( + setSearchString('')} + /> + )} + {/* Sections for policies grouped by buckets */}
- {buckets.map((bucket) => { - const bucketPolicies = get( - find(formattedStorageObjectPolicies, { name: bucket.name }), - ['policies'], - [] - ).sort((a: any, b: any) => a.name.localeCompare(b.name)) - + {filteredBucketsWithPolicies.map(({ bucket, policies }) => { return ( { {/* Only used for adding policies to buckets */} { { danger visible={!isEmpty(selectedPolicyToDelete)} title="Confirm to delete policy" - description={`This is permanent! Are you sure you want to delete the policy "${selectedPolicyToDelete.name}"`} + description={`This is permanent! Are you sure you want to delete the policy "${selectedPolicyToDelete?.name}"`} buttonLabel="Delete" buttonLoadingLabel="Deleting" onSelectCancel={onCancelPolicyDelete} onSelectConfirm={onDeletePolicy} /> -
+ ) } diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx index d4e801dec5e57..34f3a3b6692fa 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx @@ -150,6 +150,7 @@ export const CreateVectorBucketDialog = () => { className="w-fit" icon={} disabled={!canCreateBuckets} + tabIndex={!canCreateBuckets ? -1 : 0} onClick={() => setVisible(true)} tooltip={{ content: { diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails.tsx index d2790905194c2..5ac57c708fb7f 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketDetails.tsx @@ -49,10 +49,13 @@ import { DeleteVectorTableModal } from './DeleteVectorTableModal' import { getVectorBucketFDWSchemaName } from './VectorBuckets.utils' import { useS3VectorsWrapperExtension } from './useS3VectorsWrapper' import { useS3VectorsWrapperInstance } from './useS3VectorsWrapperInstance' +import { useSelectedVectorBucket } from './useSelectedVectorBuckets' export const VectorBucketDetails = () => { const router = useRouter() const { ref: projectRef, bucketId } = useParams() + // [Joshen] Use the list buckets to verify that the bucket exists first before fetching bucket details + const { data: _bucket, isSuccess } = useSelectedVectorBucket() const [filterString, setFilterString] = useState('') const [showDeleteModal, setShowDeleteModal] = useState(false) @@ -63,7 +66,10 @@ export const VectorBucketDetails = () => { error: bucketError, isSuccess: isSuccessBucket, isError: isErrorBucket, - } = useVectorBucketQuery({ projectRef, vectorBucketName: bucketId }) + } = useVectorBucketQuery( + { projectRef, vectorBucketName: bucketId }, + { enabled: isSuccess && !!_bucket } + ) const { data, isLoading: isLoadingIndexes } = useVectorBucketsIndexesQuery({ projectRef, diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx index 0be16217f3146..329b41e47a1a2 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx @@ -13,7 +13,6 @@ import { Badge, Button, Card, - cn, Table, TableBody, TableCell, @@ -157,18 +156,23 @@ export const VectorsBuckets = () => { const created = +bucket.creationTime * 1000 return ( - + handleBucketNavigation(name, event)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + handleBucketNavigation(name, event) + } + }} + tabIndex={0} + >

{name}

-

diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/useSelectedVectorBuckets.ts b/apps/studio/components/interfaces/Storage/VectorBuckets/useSelectedVectorBuckets.ts new file mode 100644 index 0000000000000..70f78febcda63 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/useSelectedVectorBuckets.ts @@ -0,0 +1,15 @@ +import { useParams } from 'common' +import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query' + +export const useSelectedVectorBucket = () => { + const { ref: projectRef, bucketId } = useParams() + + return useVectorBucketsQuery( + { projectRef }, + { + select(data) { + return data.vectorBuckets.find((x) => x.vectorBucketName === bucketId) + }, + } + ) +} diff --git a/apps/studio/components/to-be-cleaned/NoSearchResults.tsx b/apps/studio/components/to-be-cleaned/NoSearchResults.tsx deleted file mode 100644 index 4214b8a92f8b5..0000000000000 --- a/apps/studio/components/to-be-cleaned/NoSearchResults.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { BASE_PATH } from 'lib/constants' -import SVG from 'react-inlinesvg' - -/** - * To be deprecated in favor of NoSearchResults in components/ui - */ -export const NoSearchResults = () => { - return ( -

- - code.replace(/svg/, 'svg className="mb-2 w-16 h-16 text-color-inherit"') - } - /> -

- Hmm, we couldn't find any results that match your query. -

-
- ) -} - -export default NoSearchResults diff --git a/apps/studio/components/ui/NoSearchResults.tsx b/apps/studio/components/ui/NoSearchResults.tsx index d307c8535d5fa..ce44e4b1ae21e 100644 --- a/apps/studio/components/ui/NoSearchResults.tsx +++ b/apps/studio/components/ui/NoSearchResults.tsx @@ -32,5 +32,3 @@ export const NoSearchResults = ({
) } - -export default NoSearchResults diff --git a/apps/studio/data/database-policies/database-policies-query.ts b/apps/studio/data/database-policies/database-policies-query.ts index d635f4fa131fa..22f54eb9be9a1 100644 --- a/apps/studio/data/database-policies/database-policies-query.ts +++ b/apps/studio/data/database-policies/database-policies-query.ts @@ -7,7 +7,7 @@ import { PROJECT_STATUS } from 'lib/constants' import type { ResponseError, UseCustomQueryOptions } from 'types' import { databasePoliciesKeys } from './keys' -export type DatabasePoliciesVariables = { +type DatabasePoliciesVariables = { projectRef?: string connectionString?: string | null schema?: string diff --git a/apps/studio/pages/organizations.tsx b/apps/studio/pages/organizations.tsx index 2cd7e9426992f..50ecaa90afcb2 100644 --- a/apps/studio/pages/organizations.tsx +++ b/apps/studio/pages/organizations.tsx @@ -10,7 +10,7 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { withAuth } from 'hooks/misc/withAuth' diff --git a/apps/studio/pages/project/[ref]/integrations/index.tsx b/apps/studio/pages/project/[ref]/integrations/index.tsx index 482c9c835ddc0..d9965bc676b5d 100644 --- a/apps/studio/pages/project/[ref]/integrations/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/index.tsx @@ -13,7 +13,7 @@ import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' -import NoSearchResults from 'components/ui/NoSearchResults' +import { NoSearchResults } from 'components/ui/NoSearchResults' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Input } from 'ui-patterns/DataInputs/Input' diff --git a/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx index eb5cf1f236df1..e5d52bbc8b83b 100644 --- a/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx @@ -1,5 +1,10 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { toast } from 'sonner' + import { useParams } from 'common' import { AnalyticBucketDetails } from 'components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails' +import { useSelectedAnalyticsBucket } from 'components/interfaces/Storage/AnalyticsBuckets/useSelectedAnalyticsBucket' import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants' import DefaultLayout from 'components/layouts/DefaultLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' @@ -11,8 +16,18 @@ import type { NextPageWithLayout } from 'types' const AnalyticsBucketPage: NextPageWithLayout = () => { const config = BUCKET_TYPES.analytics - const { bucketId } = useParams() + const router = useRouter() + const { ref, bucketId } = useParams() const { data: project } = useSelectedProjectQuery() + const { data: bucket, isSuccess } = useSelectedAnalyticsBucket() + + useEffect(() => { + if (isSuccess && !bucket) { + toast.info(`Bucket "${bucketId}" does not exist in your project`) + router.push(`/project/${ref}/storage/analytics`) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess]) return ( { + const router = useRouter() const { bucketId, ref } = useParams() const { data: project } = useSelectedProjectQuery() const { projectRef } = useStorageExplorerStateSnapshot() - const { bucket, error, isSuccess, isError } = useSelectedBucket() + const { data: bucket, error, isSuccess, isError } = useSelectedBucket() const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null) const { getPolicyCount } = useStoragePolicyCounts(bucket ? [bucket as Bucket] : []) const policyCount = bucket ? getPolicyCount(bucket.id) : 0 + useEffect(() => { + if (isSuccess && !bucket) { + toast.info(`Bucket "${bucketId}" does not exist in your project`) + router.push(`/project/${ref}/storage/files`) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess]) + // [Joshen] Checking against projectRef from storage explorer to check if the store has initialized if (!project || !projectRef || !isSuccess) return null @@ -45,15 +56,6 @@ const BucketPage: NextPageWithLayout = () => { return } - // If the bucket is not found or the bucket type is ANALYTICS or VECTOR, show an error message - if (!bucket || ('type' in bucket && bucket.type !== 'STANDARD')) { - return ( -
-

Bucket "{bucketId}" cannot be found

-
- ) - } - return ( <> { className="[&>div:first-child]:!border-b-0" // Override the border-b from ScaffoldContainer title={
- {bucket.name} - {bucket.public && ( + {bucketId} + {bucket?.public && ( Public @@ -98,7 +100,11 @@ const BucketPage: NextPageWithLayout = () => { ) : undefined } > - Policies + + Policies + @@ -134,9 +140,11 @@ const BucketPage: NextPageWithLayout = () => { } > -
- -
+ {!!bucket && ( +
+ +
+ )} {bucket && ( diff --git a/apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx index e6588e68a9a9e..dba41d5b17b5d 100644 --- a/apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx @@ -1,5 +1,10 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { toast } from 'sonner' + import { useParams } from 'common' import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants' +import { useSelectedVectorBucket } from 'components/interfaces/Storage/VectorBuckets/useSelectedVectorBuckets' import { VectorBucketDetails } from 'components/interfaces/Storage/VectorBuckets/VectorBucketDetails' import DefaultLayout from 'components/layouts/DefaultLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' @@ -11,9 +16,20 @@ import type { NextPageWithLayout } from 'types' const VectorsBucketPage: NextPageWithLayout = () => { const config = BUCKET_TYPES['vectors'] - const { bucketId } = useParams() + const router = useRouter() + const { ref, bucketId } = useParams() const { projectRef } = useStorageExplorerStateSnapshot() + const { data: bucket, isSuccess } = useSelectedVectorBucket() + + useEffect(() => { + if (isSuccess && !bucket) { + toast.info(`Bucket "${bucketId}" does not exist in your project`) + router.push(`/project/${ref}/storage/vectors`) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess]) + return ( + We needed a system that could handle serious performance and security requirements — without + slowing down our developers. Supabase has given us both. + + +[Phoenix Energy](https://www.phoenixenergy.com/) (formerly Phoenix Capital Group) operates across oil +and gas drilling, mineral rights acquisition, and direct investment through corporate bonds. The team +rebranded in early 2024 to reflect its evolution into a full-fledged energy company. + +Facing MongoDB's SDK and Data API deprecation with only 12 months to migrate, Phoenix Energy's +seven-person engineering team rebuilt their entire data infrastructure on **Supabase**, completing the +migration a month ahead of schedule while maintaining zero downtime for their investor-facing +applications. + +## The challenge + +Phoenix Energy ran three business-critical applications on MongoDB's Data API, SDKs, and +authentication system: an investor portal at [invest.phoenixenergy.com](https://invest.phoenixenergy.com), +an internal investment admin system, and Ark, a custom-built CRM that replaced Salesforce. + +When MongoDB announced the immediate deprecation of the SDKs and Data API in September 2024, the +team faced several critical challenges: + +- **Complete infrastructure replacement** across authentication, database, storage, and API endpoints +- **Three production applications** dependent on the deprecated stack +- **Compressed migration window** with only 12 months to evaluate, migrate, and validate +- **Performance concerns** after early testing showed 20-30× slower response times on suggested alternatives +- **User experience risk** where any friction in the investor portal could cost conversions + + + We had initially chosen MongoDB because it was all-in-one: auth, storage, database, endpoints. + With that going away, we had to rethink our entire backend infrastructure. But it also gave us an + opportunity to build something stronger. And that’s when we found Supabase. + + +Throughout the evaluation period—October through December 2024—the team explored MongoDB via Azure, +Firebase, and Fauna. Each alternative came with significant drawbacks, from performance issues to +unclear answers about long-term stability. + +## Choosing Supabase + +Supabase emerged as the clear choice, offering the same all-in-one appeal that initially drew Phoenix +to MongoDB, but backed by a proven Postgres foundation. + + + Supabase is exactly what once made MongoDB attractive. We had auth again, all in one place. We had + database, we had storage. And it's built on open source Postgres, which has been tried and tested + for many years. That gave us confidence. + + +**Why Supabase** + +- **Proven foundation:** Postgres delivered the stability and maturity the team needed for critical workloads. +- **All-in-one platform:** Auth, database, APIs, and storage under one roof reduced dependencies and operational overhead. +- **Approachable learning curve:** Documentation and tooling enabled engineers of varying experience levels to onboard quickly. +- **Impressive performance:** Proof-of-concept tests exceeded MongoDB benchmarks before optimization. +- **Active roadmap:** Rapid iteration and an engaged community signaled long-term platform health. +- **Direct support:** Slack channels with Supabase engineers delivered real-time guidance during migration. + +The team also appreciated that rising engineers could quickly become productive with Supabase's +approachable API design and clear documentation, limiting onboarding friction during a high-pressure +timeline. + +## The approach + +With an August 2025 deadline, Phoenix Energy executed a parallel migration across all three +applications using a tightly coordinated plan: + +- **Custom migration tooling** + - Automated data cycling, syncing, testing, and validation between MongoDB and Supabase + - Continuous integration checks ensured parity across codebases during the rewrite +- **Complete query rewrite** + - Translated every NoSQL query into SQL patterns while preserving feature parity + - Introduced relational models that improved long-term data clarity and maintainability +- **Weekend cutover** + - Final data migration ran over a weekend, with the switch flipped Sunday night + - Only one day of minor issues surfaced during transition, resolved before Monday investor traffic +- **Direct team support** + - Supabase engineers provided hands-on guidance via connected Slack channels + - Best practices around security, indexing, and performance tuning were incorporated in real time + + + We transitioned three applications in six months. We built migration tooling, rewrote every + database query, and flipped the switch over a weekend. When everyone got back to work Monday, + nobody really knew any different. The systems just worked. + + +## The results + +Phoenix Energy completed the migration in August 2024—one month ahead of schedule—with measurable wins +for both the business and engineering team. + +- **Zero user-facing disruption** during and after migration across all investor applications +- **Improved performance** with noticeably faster responses ahead of further optimization work +- **Stronger foundation** ready for analytics and AI workloads powered by relational data models +- **Team confidence** to decommission MongoDB instances immediately after cutover +- **Six-month execution** from project kickoff to production transition with a seven-person team +- **Better developer experience** through clearer SQL workflows and faster debugging cycles + + + We've been very happy with how that's gone. Our experience with the Supabase team has been great. + Being able to get responses directly through the connected Slack channel made a huge difference. + + +## What's next + +With a reliable Supabase foundation in place, Phoenix Energy is focused on expanding its data +capabilities and responsibly exploring AI: + +- **Analytics infrastructure:** The team plans to use upcoming Supabase ETL features to build an end-to-end analytics stack without introducing new syncing layers. +- **AI workloads:** Engineers are testing vector databases and MCP servers in isolated Supabase environments, ensuring compliance with SEC regulations before production rollout. + + + Supabase ETL is going to be really interesting for us. I'd love for everything to live in the same + place so the team can focus on building impactful tools. It really does feel like Supabase is just + getting started and I am excited about how we can grow with them from here. + diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index 395ff6b4a9317..3f41cfd4f59f8 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Tue, 21 Oct 2025 00:00:00 -0700 + Thu, 13 Nov 2025 00:00:00 -0700 + https://supabase.com/customers/phoenix-energy + Phoenix Energy completes critical infrastructure migration in six months with Supabase + https://supabase.com/customers/phoenix-energy + Phoenix Energy rebuilt its infrastructure on Supabase, completing a full-stack migration from MongoDB ahead of deadline with zero downtime. + Thu, 13 Nov 2025 00:00:00 -0700 + + https://supabase.com/customers/rally Rally builds a pan-European fleet payments platform on Supabase https://supabase.com/customers/rally diff --git a/apps/www/public/images/blog/avatars/kris-woods-phoenix-energy.jpg b/apps/www/public/images/blog/avatars/kris-woods-phoenix-energy.jpg new file mode 100644 index 0000000000000..039e4a3274d56 Binary files /dev/null and b/apps/www/public/images/blog/avatars/kris-woods-phoenix-energy.jpg differ diff --git a/apps/www/public/images/customers/logos/light/phoenix-energy.png b/apps/www/public/images/customers/logos/light/phoenix-energy.png new file mode 100644 index 0000000000000..e549d15e60662 Binary files /dev/null and b/apps/www/public/images/customers/logos/light/phoenix-energy.png differ diff --git a/apps/www/public/images/customers/logos/phoenix-energy.png b/apps/www/public/images/customers/logos/phoenix-energy.png new file mode 100644 index 0000000000000..805015ed09bee Binary files /dev/null and b/apps/www/public/images/customers/logos/phoenix-energy.png differ