Files
supabase/apps/studio/components/interfaces/Settings/API/UnsafeEntitiesConfirmModal.tsx
Gildas Garcia 0713a1efc1 chore: remove shadcn suffix for Input, Textarea, Alert and Collapsible (#45867)
## Problem

Now that we migrated old components to their new shadcn alternatives, we
don't need the `_Shadcn_` suffix anymore.

## Solution

Remove it

<img width="659" height="609" alt="image"
src="https://github.com/user-attachments/assets/2d7271a9-066a-4dcc-92fe-729b106d2c2f"
/>
2026-05-15 14:55:37 +02:00

174 lines
5.5 KiB
TypeScript

import { ChevronRight, ChevronUp } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { type ExposedEntity } from './DataApiEnableSwitch.utils'
interface UnsafeEntitiesConfirmModalProps {
visible: boolean
loading: boolean
unsafeEntities: Array<ExposedEntity>
onCancel: () => void
onConfirm: () => void
}
const ENTITY_TYPE_META: Record<
ExposedEntity['type'],
{ heading: string; recommendation: string; docsUrl: string }
> = {
table: {
heading: 'Tables without Row Level Security',
recommendation: 'Enable RLS on these tables to control access per-row.',
docsUrl:
'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public',
},
'foreign table': {
heading: 'Foreign tables',
recommendation:
'Foreign tables do not support RLS. Revoke access from the anon and authenticated roles.',
docsUrl:
'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api',
},
'materialized view': {
heading: 'Materialized views',
recommendation:
'Materialized views do not support RLS. Revoke access from the anon and authenticated roles.',
docsUrl:
'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api',
},
view: {
heading: 'Views without SECURITY INVOKER',
recommendation:
'These views run with the permissions of the view creator, not the querying user. Set SECURITY INVOKER to enforce caller permissions.',
docsUrl:
'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view',
},
}
const ENTITY_TYPE_ORDER: Array<ExposedEntity['type']> = [
'table',
'foreign table',
'materialized view',
'view',
]
const COLLAPSE_THRESHOLD = 3
export const UnsafeEntitiesConfirmModal = ({
visible,
loading,
unsafeEntities,
onCancel,
onConfirm,
}: UnsafeEntitiesConfirmModalProps) => {
const groupedEntities = useMemo(() => {
const groups = new Map<ExposedEntity['type'], Array<ExposedEntity>>()
for (const entity of unsafeEntities) {
const group = groups.get(entity.type)
if (group) {
group.push(entity)
} else {
groups.set(entity.type, [entity])
}
}
return ENTITY_TYPE_ORDER.filter((type) => groups.has(type)).map((type) => ({
type,
...ENTITY_TYPE_META[type],
entities: groups.get(type) ?? [],
}))
}, [unsafeEntities])
return (
<ConfirmationModal
variant="warning"
visible={visible}
loading={loading}
title="Insecure objects detected"
confirmLabel="Enable Data API"
confirmLabelLoading="Enabling"
onCancel={onCancel}
onConfirm={onConfirm}
className="max-h-[50vh] overflow-y-auto"
>
<div className="text-sm text-foreground-light space-y-4">
<p>
The following objects will be publicly accessible through the Data API and are insecure.
</p>
{groupedEntities.map(({ type, heading, recommendation, docsUrl, entities }) => (
<div key={type} className="space-y-1">
<h4 className="text-foreground font-medium">{heading}</h4>
<EntityList entities={entities} />
<p className="text-foreground-lighter text-xs">
{recommendation}{' '}
<a
href={docsUrl}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Learn more
</a>
</p>
</div>
))}
</div>
</ConfirmationModal>
)
}
const EntityListItem = ({ entity }: { entity: ExposedEntity }) => (
<li>
<code className="text-xs">
{entity.schema}.{entity.name}
</code>
</li>
)
const EntityList = ({ entities }: { entities: Array<ExposedEntity> }) => {
const [open, setOpen] = useState(false)
const shouldCollapse = entities.length > COLLAPSE_THRESHOLD
const visibleEntities = entities.slice(0, COLLAPSE_THRESHOLD)
const hiddenEntities = entities.slice(COLLAPSE_THRESHOLD)
if (!shouldCollapse) {
return (
<ul className="list-disc pl-5 space-y-0.5">
{entities.map((entity) => (
<EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
))}
</ul>
)
}
return (
<Collapsible open={open} onOpenChange={setOpen}>
<ul className="list-disc pl-5 space-y-0.5">
{visibleEntities.map((entity) => (
<EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
))}
</ul>
<CollapsibleContent className="transition-all data-closed:animate-collapsible-up data-open:animate-collapsible-down">
<ul className="list-disc pl-5 space-y-0.5">
{hiddenEntities.map((entity) => (
<EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
))}
</ul>
</CollapsibleContent>
<CollapsibleTrigger asChild>
<Button
type="text"
size="tiny"
className="px-0 h-auto text-xs text-foreground-lighter hover:text-foreground"
>
<div className="flex items-center gap-1">
{open ? <ChevronUp size={12} /> : <ChevronRight size={12} />}
<span>{open ? 'Show less' : `Show ${hiddenEntities.length} more`}</span>
</div>
</Button>
</CollapsibleTrigger>
</Collapsible>
)
}