Files
supabase/apps/studio/components/interfaces/App/CommandMenu/PolicySearchResults.tsx
T
Monica Khoury 2afb87af05 fix(ui): add bottom padding to scroll container in RLS search (#48759)
Fixes FE-4075. 

## What kind of change does this PR introduce?

The footer displaying the total number of RLS policies overlaps the last
search result in the RLS Policy Search dialog. As a result, the last
policy entry is partially hidden and cannot be fully read when scrolling
to the bottom.

## What is the current behavior?

The search results container now reserves space for the footer,
preventing it from overlapping the last search result. All policy
entries remain fully visible when scrolling to the bottom.

<img width="400" height="300" alt="image"
src="https://github.com/user-attachments/assets/46529ca4-bdce-4fa2-b0ba-ea87e769cc24"
/>

## What is the new behavior?

<img width="400" height="300" alt="CleanShot 2026-08-05 at 18 19 27@2x"
src="https://github.com/user-attachments/assets/093a3f9e-fd4c-4ff6-b483-2839f3916d13"
/>

## How to test

- Open a project in the Supabase Dashboard.
- Navigate to Database → RLS Policies.
- Open the policy search dialog.
- Search for a term that returns enough results to make the list
scrollable
- Scroll to the bottom of the results.

The
[database.sql](https://gist.github.com/monicakh/49b5ff201893eb43aea329395b3f635b)
to create the tables/policies to test.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved scrolling in policy search results.
* Added spacing at the bottom so results remain visible above the fixed
footer.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-06 17:00:28 +07:00

159 lines
4.7 KiB
TypeScript

'use client'
import { useParams } from 'common'
import { Auth } from 'icons'
import { Loader2 } from 'lucide-react'
import { useMemo } from 'react'
import {
EmptyState,
ResultsList,
SkeletonResults,
type SearchResult,
} from './ContextSearchResults.shared'
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
interface PolicySearchResultsProps {
query: string
}
export function PolicySearchResults({ query }: PolicySearchResultsProps) {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const trimmedQuery = query.trim()
const {
data: policies,
isLoading: isLoadingPolicies,
isError: isErrorPolicies,
} = useDatabasePoliciesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
enabled: !!project?.ref,
}
)
const policyResults: SearchResult[] = useMemo(() => {
if (!policies) return []
const filtered = trimmedQuery
? policies.filter((policy) => {
const searchLower = trimmedQuery.toLowerCase()
const policyName = policy.name?.toLowerCase() || ''
const tableName = policy.table?.toLowerCase() || ''
const schemaName = policy.schema?.toLowerCase() || ''
const fullTableName = `${schemaName}.${tableName}`
const command = policy.command?.toLowerCase() || ''
return (
policyName.includes(searchLower) ||
tableName.includes(searchLower) ||
schemaName.includes(searchLower) ||
fullTableName.includes(searchLower) ||
command.includes(searchLower)
)
})
: policies
// Limit results for performance
return filtered.slice(0, 20).map((policy) => {
const displayName = policy.name || 'Untitled Policy'
const tableDisplay =
policy.schema && policy.schema !== 'public'
? `${policy.schema}.${policy.table}`
: policy.table || 'unknown table'
const commandDisplay = policy.command || 'ALL'
const description = `${commandDisplay} on ${tableDisplay}`
return {
id: String(policy.id),
name: displayName,
description,
}
})
}, [policies, trimmedQuery])
const totalPolicies = policies?.length ?? 0
const renderFooter = () => (
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between min-h-9 h-9 px-4 border-t bg-surface-200 text-xs text-foreground-light z-10">
<div className="flex items-center gap-x-2">
{isLoadingPolicies ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" /> Loading...
</span>
) : (
<span>
Total: {totalPolicies.toLocaleString()} polic{totalPolicies !== 1 ? 'ies' : 'y'}
</span>
)}
</div>
</div>
)
if (isLoadingPolicies) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<SkeletonResults />
</div>
{renderFooter()}
</div>
)
}
if (isErrorPolicies) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
<Auth className="h-6 w-6" strokeWidth={1.5} />
<p className="text-sm">Failed to load policies</p>
</div>
</div>
{renderFooter()}
</div>
)
}
if (policyResults.length === 0) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<EmptyState icon={Auth} label="RLS Policies" query={query} />
</div>
{renderFooter()}
</div>
)
}
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-y-auto pb-9">
<ResultsList
results={policyResults}
icon={Auth}
getRoute={(result) => {
const policy = policies?.find((p) => String(p.id) === result.id)
if (!policy || !projectRef)
return `/project/${projectRef}/database/policies` as `/${string}`
const params = new URLSearchParams()
params.set('edit', String(policy.id))
if (policy.schema) {
params.set('schema', policy.schema)
}
return `/project/${projectRef}/database/policies?${params.toString()}` as `/${string}`
}}
/>
</div>
{renderFooter()}
</div>
)
}