From eaf35c14c6d67d0f7c43cc1267de259d416c9a4c Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:47:16 +1100 Subject: [PATCH] chore(studio): filter policies by bucket name (#40456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * filter buckets * Simplify * General refactors * Smol * Add missing empty state for search * Clean up 😬 --------- Co-authored-by: Joshen Lim --- .../Auth/Policies/PolicyEditorModal/index.tsx | 6 +- .../PolicyEditorPanel/PolicyTemplates.tsx | 4 +- .../Database/Extensions/Extensions.tsx | 2 +- .../Database/Hooks/HooksList/HooksList.tsx | 2 +- .../Publications/PublicationsList.tsx | 2 +- .../Publications/PublicationsTables.tsx | 4 +- .../interfaces/Database/Roles/RolesList.tsx | 6 +- .../interfaces/Database/Tables/ColumnList.tsx | 7 +- .../Home/ProjectList/ProjectList.tsx | 2 +- .../Landing/AvailableIntegrations.tsx | 2 +- .../interfaces/Storage/Storage.utils.ts | 6 +- .../StoragePolicies/StoragePolicies.tsx | 142 +++++++++++++----- .../to-be-cleaned/NoSearchResults.tsx | 23 --- apps/studio/components/ui/NoSearchResults.tsx | 2 - .../database-policies-query.ts | 2 +- apps/studio/pages/organizations.tsx | 2 +- .../project/[ref]/integrations/index.tsx | 2 +- .../storage/files/buckets/[bucketId].tsx | 6 +- 18 files changed, 139 insertions(+), 83 deletions(-) delete mode 100644 apps/studio/components/to-be-cleaned/NoSearchResults.tsx diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 73167045ed6..5efc9527622 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 5e273759dce..bdb31dd0d3d 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 08ec649e6d2..b201b5b1b77 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 56dbdfbd013..5c19e17b08e 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 27955f326a1..59d71ef77bf 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 f543e5469dc..b1cdf2a8e9a 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 0c6fe60f15b..b33a2d1ce47 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 a4e9c23bca2..27b48c39b1d 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('')} + /> ) : (
{ * 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/StoragePolicies/StoragePolicies.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx index 2df739a44e9..9143a92a345 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/to-be-cleaned/NoSearchResults.tsx b/apps/studio/components/to-be-cleaned/NoSearchResults.tsx deleted file mode 100644 index 4214b8a92f8..00000000000 --- 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 d307c8535d5..ce44e4b1ae2 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 d635f4fa131..22f54eb9be9 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 2cd7e942699..50ecaa90afc 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 482c9c835dd..d9965bc676b 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/files/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx index 4284e98cb8b..e3777f15808 100644 --- a/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx @@ -100,7 +100,11 @@ const BucketPage: NextPageWithLayout = () => { ) : undefined } > - Policies + + Policies +