Files
Joshen Lim 7f8fb85caf Joshen/fe 3879 schema not exposed warning is unnecessarily repeated in (#47868)
Just a tiny nit i came across - realised that if the schema is not
exposed via the API, the warning that we show on the policies page RE
data not being selectable is repeated for each table which imo seems
unnecessary.

Opting for a single admonition at the top instead

## Before
<img width="1085" height="744" alt="image"
src="https://github.com/user-attachments/assets/7bd8cf7b-f5a8-47d6-b41f-13fc4782ed8b"
/>

### After
<img width="1080" height="673" alt="image"
src="https://github.com/user-attachments/assets/c839f502-42ff-4184-ad07-0ff2fd2d2676"
/>


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

* **New Features**
* Added an on-screen warning for tables whose schemas aren’t exposed via
the project Data APIs, including a link to Data API settings.
* **UI Improvements**
* Refined the “filter entity types” control’s tooltip behavior and
updated the popover header text.
* **Bug Fixes**
* Improved Data API/RLS status messaging by removing the prior “schema
not exposed” outcome and showing “unknown” when access can’t be
determined.
* Consolidated policy warning rendering to avoid duplicated or
inconsistent messages.
* **Tests**
* Updated policy/admonition helper tests to match the revised status and
message behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 13:47:05 +08:00

171 lines
5.7 KiB
TypeScript

import { useParams } from 'common'
import { noop } from 'lodash'
import { memo, useMemo } from 'react'
import {
Card,
CardContent,
CardHeader,
cn,
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { usePoliciesData } from '../PoliciesDataContext'
import { PolicyRow } from './PolicyRow'
import type { PolicyTable } from './PolicyTableRow.types'
import type { Policy } from './PolicyTableRow.utils'
import { getTableAdmonitionMessage, getTableDataApiStatus } from './PolicyTableRow.utils'
import { PolicyTableRowHeader } from './PolicyTableRowHeader'
import { AlertError } from '@/components/ui/AlertError'
import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export interface PolicyTableRowProps {
table: PolicyTable
isLocked: boolean
onSelectToggleRLS: (table: PolicyTable) => void
onSelectCreatePolicy: (table: PolicyTable) => void
onSelectEditPolicy: (policy: Policy) => void
onSelectDeletePolicy: (policy: Policy) => void
}
const PolicyTableRowComponent = ({
table,
isLocked,
onSelectToggleRLS = noop,
onSelectCreatePolicy = noop,
onSelectEditPolicy = noop,
onSelectDeletePolicy = noop,
}: PolicyTableRowProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { getPoliciesForTable, isPoliciesLoading, isPoliciesError, policiesError } =
usePoliciesData()
const policies = useMemo(
() => getPoliciesForTable(table.schema, table.name),
[getPoliciesForTable, table.schema, table.name]
)
// [Joshen] Classification is more granular than "RLS on/off" alone — it also considers
// schema exposure and whether anon/authenticated/service_role actually have grants.
// Ideally we'd rely on the security lints, but they only look at the public schema and
// ignore roles. Once the lints cover both, we can switch to them as the source of truth.
const tableNames = useMemo(() => [table.name], [table.name])
const {
data: apiAccessMap,
isPending: isLoadingRolesAccess,
isError: isRolesAccessError,
} = useTableApiAccessQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schemaName: table.schema,
tableNames,
})
const status = useMemo(
() =>
getTableDataApiStatus({
apiAccessData: apiAccessMap?.[table.name],
isRLSEnabled: table.rls_enabled,
policiesCount: policies.length,
}),
[apiAccessMap, table.name, table.rls_enabled, policies.length]
)
const hasApiAccess =
status === 'publicly-readable' || status === 'locked-by-rls' || status === 'secured'
const isPubliclyReadable = status === 'publicly-readable'
const isRealtimeSchema = table.schema === 'realtime'
const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked
const showPolicies = !isPoliciesLoading && !isPoliciesError && !isLoadingRolesAccess
const admonitionMessage = useMemo(() => getTableAdmonitionMessage({ status, ref }), [status, ref])
return (
<Card className={cn(isPubliclyReadable && 'border-warning-500')}>
<CardHeader
className={cn('py-3 px-4', status !== 'secured' && status !== 'unknown' && 'border-b-0')}
>
<PolicyTableRowHeader
table={table}
isLocked={isLocked}
hasApiAccess={hasApiAccess}
isLoadingApiAccess={isLoadingRolesAccess}
onSelectToggleRLS={onSelectToggleRLS}
onSelectCreatePolicy={onSelectCreatePolicy}
/>
</CardHeader>
{!isLoadingRolesAccess && !isRolesAccessError && admonitionMessage !== null && (
<Admonition
showIcon={false}
type={isPubliclyReadable ? 'warning' : 'default'}
className="border-0 border-y rounded-none min-h-12 flex items-center"
>
{admonitionMessage}
</Admonition>
)}
{(isPoliciesLoading || isLoadingRolesAccess) && (
<CardContent>
<ShimmeringLoader />
</CardContent>
)}
{isPoliciesError && (
<CardContent>
<AlertError
className="border-0 rounded-none"
error={policiesError}
subject="Failed to retrieve policies"
/>
</CardContent>
)}
{showPolicies && (
<CardContent className="p-0">
{policies.length === 0 ? (
<p className="text-foreground-lighter text-sm p-4">No policies created yet</p>
) : (
<Table className="table-fixed">
<TableHeader>
<TableRow>
<TableHead className="w-[40%]">Name</TableHead>
<TableHead className="w-[20%]">Command</TableHead>
<TableHead className="w-[30%]">Applied to</TableHead>
<TableHead className="text-right">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{policies.map((policy) => (
<PolicyRow
key={policy.id}
policy={policy}
isLocked={isTableLocked}
onSelectEditPolicy={onSelectEditPolicy}
onSelectDeletePolicy={onSelectDeletePolicy}
/>
))}
</TableBody>
</Table>
)}
</CardContent>
)}
</Card>
)
}
export const PolicyTableRow = memo(PolicyTableRowComponent)
PolicyTableRow.displayName = 'PolicyTableRow'