mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Refine the policies page (#38204)
* refine the policies page * minor fixes * Fix TS * Do not transform table name in policy row table header --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
39df774af6
commit
db4ae7e326
9 files changed
+287
-301
No files matched your search
@@ -1,7 +1,6 @@
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -11,32 +10,34 @@ import {
|
||||
PolicyTableRowProps,
|
||||
} from 'components/interfaces/Auth/Policies/PolicyTableRow'
|
||||
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { NoSearchResults } from 'components/ui/NoSearchResults'
|
||||
import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation'
|
||||
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Button, Card, CardContent } from 'ui'
|
||||
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
|
||||
|
||||
interface PoliciesProps {
|
||||
search?: string
|
||||
schema: string
|
||||
tables: PolicyTableRowProps['table'][]
|
||||
hasTables: boolean
|
||||
isLocked: boolean
|
||||
onSelectCreatePolicy: (table: string) => void
|
||||
onSelectEditPolicy: (policy: PostgresPolicy) => void
|
||||
onResetSearch?: () => void
|
||||
}
|
||||
|
||||
const Policies = ({
|
||||
export const Policies = ({
|
||||
search,
|
||||
schema,
|
||||
tables,
|
||||
hasTables,
|
||||
isLocked,
|
||||
onSelectCreatePolicy,
|
||||
onSelectEditPolicy: onSelectEditPolicyAI,
|
||||
onResetSearch,
|
||||
}: PoliciesProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
@@ -115,40 +116,21 @@ const Policies = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (tables.length === 0) {
|
||||
if (!hasTables) {
|
||||
return (
|
||||
<div className="flex-grow flex items-center justify-center">
|
||||
<ProductEmptyState
|
||||
size="large"
|
||||
title="Row-Level Security (RLS) Policies"
|
||||
ctaButtonLabel="Create a table"
|
||||
infoButtonLabel="What is RLS?"
|
||||
infoButtonUrl="https://supabase.com/docs/guides/auth/row-level-security"
|
||||
onClickCta={() => router.push(`/project/${ref}/editor`)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<InformationBox
|
||||
title="What are policies?"
|
||||
icon={<HelpCircle size={14} strokeWidth={2} />}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">
|
||||
Policies restrict, on a per-user basis, which rows can be returned by normal
|
||||
queries, or inserted, updated, or deleted by data modification commands.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
This is also known as Row-Level Security (RLS). Each policy is attached to a
|
||||
table, and the policy is executed each time its accessed.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Create a table in this schema first before creating a policy.
|
||||
</p>
|
||||
</div>
|
||||
</ProductEmptyState>
|
||||
</div>
|
||||
<Card className="w-full bg-transparent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-8">
|
||||
<h2 className="heading-default">No tables to create policies for</h2>
|
||||
|
||||
<p className="text-sm text-foreground-light text-center mb-4">
|
||||
RLS Policies control per-user access to table rows. Create a table in this schema first
|
||||
before creating a policy.
|
||||
</p>
|
||||
<Button asChild type="default">
|
||||
<Link href={`/project/${ref}/editor`}>Create a table</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,7 +152,7 @@ const Policies = ({
|
||||
</section>
|
||||
))
|
||||
) : hasTables ? (
|
||||
<NoSearchResults />
|
||||
<NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -202,5 +184,3 @@ const Policies = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Policies
|
||||
@@ -4,7 +4,6 @@ import { noop } from 'lodash'
|
||||
import { Edit, MoreVertical, Trash } from 'lucide-react'
|
||||
|
||||
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
@@ -13,6 +12,8 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
cn,
|
||||
TableRow,
|
||||
TableCell,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -57,58 +58,50 @@ const PolicyRow = ({
|
||||
(policy.roles.includes('authenticated') || policy.roles.includes('public'))
|
||||
|
||||
return (
|
||||
<Panel.Content
|
||||
className={cn(
|
||||
'flex border-overlay',
|
||||
'w-full last:border-0 space-x-4 border-b py-4 lg:items-center'
|
||||
)}
|
||||
>
|
||||
<div className="flex grow flex-col gap-y-1">
|
||||
<div className="flex items-start gap-x-4">
|
||||
<p className="font-mono text-xs text-foreground-light translate-y-[2px] min-w-12">
|
||||
{policy.command}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<Button
|
||||
type="text"
|
||||
className="h-auto text-foreground text-sm border-none p-0 hover:bg-transparent justify-start"
|
||||
onClick={() => onSelectEditPolicy(policy)}
|
||||
>
|
||||
{policy.name}
|
||||
</Button>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<div className="text-foreground-lighter text-sm">
|
||||
Applied to:{' '}
|
||||
{policy.roles.slice(0, 3).map((role, i) => (
|
||||
<span key={`policy-${role}-${i}`}>
|
||||
<code className="text-foreground-light text-xs">{role}</code>
|
||||
{i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '}
|
||||
</span>
|
||||
))}
|
||||
{policy.roles.length > 1 ? 'roles' : 'role'}
|
||||
</div>
|
||||
{policy.roles.length > 3 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<code key="policy-etc" className="text-foreground-light text-xs">
|
||||
+ {policy.roles.length - 3} more roles
|
||||
</code>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center">
|
||||
{policy.roles.slice(3).join(', ')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TableRow>
|
||||
<TableCell className="w-[40%] truncate">
|
||||
<div className="flex items-center gap-x-2 min-w-0">
|
||||
<Button
|
||||
type="text"
|
||||
className="text-foreground text-sm p-0 hover:bg-transparent w-full truncate justify-start"
|
||||
onClick={() => onSelectEditPolicy(policy)}
|
||||
>
|
||||
{policy.name}
|
||||
</Button>
|
||||
{appliesToAnonymousUsers ? (
|
||||
<Badge color="yellow">Applies to anonymous users</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[20%] truncate">
|
||||
<code className="text-foreground-light text-xs">{policy.command}</code>
|
||||
</TableCell>
|
||||
<TableCell className="w-[30%] truncate">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<div className="text-foreground-lighter text-sm">
|
||||
{policy.roles.slice(0, 3).map((role, i) => (
|
||||
<span key={`policy-${role}-${i}`}>
|
||||
<code className="text-foreground-light text-xs">{role}</code>
|
||||
{i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '}
|
||||
</span>
|
||||
))}
|
||||
{policy.roles.length > 1 ? 'roles' : 'role'}
|
||||
</div>
|
||||
{policy.roles.length > 3 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<code key="policy-etc" className="text-foreground-light text-xs">
|
||||
+ {policy.roles.length - 3} more roles
|
||||
</code>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center">
|
||||
{policy.roles.slice(3).join(', ')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-0 text-right whitespace-nowrap">
|
||||
{!isLocked && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -127,9 +120,9 @@ const PolicyRow = ({
|
||||
name: `Update policy ${policy.name}`,
|
||||
open: true,
|
||||
sqlSnippets: [sql],
|
||||
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
|
||||
initialInput: `Update the policy with name \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
|
||||
suggestions: {
|
||||
title: `I can help you make a change to the policy "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
|
||||
title: `I can help you make a change to the policy \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
|
||||
prompts: [
|
||||
{
|
||||
label: 'Improve Policy',
|
||||
@@ -169,8 +162,8 @@ const PolicyRow = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+13
-22
@@ -1,13 +1,13 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop } from 'lodash'
|
||||
import { Lock, Unlock } from 'lucide-react'
|
||||
import { Lock, Table } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { AiIconAnimation, Badge } from 'ui'
|
||||
import { AiIconAnimation, Badge, CardTitle } from 'ui'
|
||||
|
||||
interface PolicyTableRowHeaderProps {
|
||||
table: {
|
||||
@@ -54,28 +54,19 @@ const PolicyTableRowHeader = ({
|
||||
<EditorTablePageLink
|
||||
projectRef={ref}
|
||||
id={String(table.id)}
|
||||
className="flex items-center gap-x-2"
|
||||
className="flex items-center gap-x-3"
|
||||
>
|
||||
{table.rls_enabled ? (
|
||||
<div className="flex items-center gap-x-1 text-xs">
|
||||
<Lock size={14} strokeWidth={2} className="text-brand" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-1 text-xs">
|
||||
<Unlock size={14} strokeWidth={2} className="text-warning-600" />
|
||||
</div>
|
||||
)}
|
||||
<h4 className="m-0">{table.name}</h4>
|
||||
<Table strokeWidth={1.5} size={16} className="text-foreground-muted" />
|
||||
<CardTitle className="m-0 normal-case">{table.name}</CardTitle>
|
||||
{!table.rls_enabled && <Badge variant="warning">RLS Disabled</Badge>}
|
||||
</EditorTablePageLink>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isTableLocked && (
|
||||
<Badge>
|
||||
<span className="flex gap-2 items-center text-xs uppercase text-foreground-lighter">
|
||||
<Lock size={12} /> Locked
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isTableLocked && (
|
||||
<Badge>
|
||||
<span className="flex gap-2 items-center text-xs uppercase text-foreground-lighter">
|
||||
<Lock size={12} /> Locked
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!isTableLocked && (
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { noop } from 'lodash'
|
||||
import { Info } from 'lucide-react'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
|
||||
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
||||
import { useTableRolesAccessQuery } from 'data/tables/table-roles-access-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
cn,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import PolicyRow from './PolicyRow'
|
||||
import PolicyTableRowHeader from './PolicyTableRowHeader'
|
||||
@@ -73,85 +83,86 @@ export const PolicyTableRow = ({
|
||||
.filter((policy) => policy.schema === table.schema && policy.table === table.name)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const rlsEnabledNoPolicies = isRLSEnabled && policies.length === 0
|
||||
const isRealtimeSchema = table.schema === 'realtime'
|
||||
const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
|
||||
const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="!m-0"
|
||||
title={
|
||||
<Card className={cn(isPubliclyReadableWritable && 'border-warning-500')}>
|
||||
<CardHeader
|
||||
className={cn(
|
||||
'py-3 px-4',
|
||||
(isPubliclyReadableWritable || rlsEnabledNoPolicies) && 'border-b-0'
|
||||
)}
|
||||
>
|
||||
<PolicyTableRowHeader
|
||||
table={table}
|
||||
isLocked={isLocked}
|
||||
onSelectToggleRLS={onSelectToggleRLS}
|
||||
onSelectCreatePolicy={onSelectCreatePolicy}
|
||||
/>
|
||||
}
|
||||
>
|
||||
</CardHeader>
|
||||
|
||||
{(isPubliclyReadableWritable || rlsEnabledNoPolicies) && (
|
||||
<div
|
||||
className={cn(
|
||||
'dark:bg-alternative-200 bg-surface-200 px-6 py-2 text-xs flex items-center gap-2',
|
||||
policies.length === 0 ? '' : 'border-b'
|
||||
)}
|
||||
<Alert_Shadcn_
|
||||
className="border-0 rounded-none mb-0 border-b border-t"
|
||||
variant={isPubliclyReadableWritable ? 'warning' : 'default'}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-1.5 h-1.5 rounded-full bg-warning-600 ',
|
||||
rlsEnabledNoPolicies && 'bg-selection'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn('font-bold text-warning-600', rlsEnabledNoPolicies && 'text-foreground')}
|
||||
>
|
||||
{isPubliclyReadableWritable ? 'Warning' : 'Note'}:
|
||||
</span>{' '}
|
||||
<span className="text-foreground-light">
|
||||
<AlertDescription_Shadcn_>
|
||||
{isPubliclyReadableWritable
|
||||
? 'Row Level Security is disabled. Your table is publicly readable and writable.'
|
||||
: 'Row Level Security is enabled, but no policies exist. No data will be selectable via Supabase APIs.'}
|
||||
</span>
|
||||
{isPubliclyReadableWritable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-3 h-3" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[400px]">
|
||||
Anyone with the project's anonymous key can modify or delete your data. Enable RLS
|
||||
and create access policies to keep your data secure.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
? "Anyone with your project's anonymous key can read, modify, or delete your data."
|
||||
: 'No data will be selectable via Supabase APIs because RLS is enabled but no policies have been created yet.'}
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="px-6 py-4">
|
||||
<CardContent>
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<AlertError
|
||||
className="border-0 rounded-none"
|
||||
error={error}
|
||||
subject="Failed to retrieve policies"
|
||||
/>
|
||||
<CardContent>
|
||||
<AlertError
|
||||
className="border-0 rounded-none"
|
||||
error={error}
|
||||
subject="Failed to retrieve policies"
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{policies.length === 0 && (
|
||||
<div className="px-6 py-4 flex flex-col gap-y-3">
|
||||
<p className="text-foreground-lighter text-sm">No policies created yet</p>
|
||||
</div>
|
||||
<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="w-0 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>
|
||||
)}
|
||||
{policies?.map((policy) => (
|
||||
<PolicyRow
|
||||
key={policy.id}
|
||||
isLocked={isLocked}
|
||||
policy={policy}
|
||||
onSelectEditPolicy={onSelectEditPolicy}
|
||||
onSelectDeletePolicy={onSelectDeletePolicy}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</CardContent>
|
||||
)}
|
||||
</Panel>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Policies from 'components/interfaces/Auth/Policies/Policies'
|
||||
import { Policies } from 'components/interfaces/Auth/Policies/Policies'
|
||||
import { PolicyEditorPanel } from 'components/interfaces/Auth/Policies/PolicyEditorPanel'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import SVG from 'react-inlinesvg'
|
||||
|
||||
// [Joshen] To be deprecated in favor of NoSearchResults in components/ui
|
||||
|
||||
/**
|
||||
* To be deprecated in favor of NoSearchResults in components/ui
|
||||
*/
|
||||
export const NoSearchResults = () => {
|
||||
return (
|
||||
<div className="flex h-64 flex-col items-center justify-center">
|
||||
|
||||
@@ -6,7 +6,11 @@ export interface NoSearchResultsProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NoSearchResults = ({ searchString, onResetFilter, className }: NoSearchResultsProps) => {
|
||||
export const NoSearchResults = ({
|
||||
searchString,
|
||||
onResetFilter,
|
||||
className,
|
||||
}: NoSearchResultsProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -35,6 +35,7 @@ interface SchemaSelectorProps {
|
||||
onSelectSchema: (name: string) => void
|
||||
onSelectCreateSchema?: () => void
|
||||
portal?: boolean
|
||||
align?: 'start' | 'end'
|
||||
}
|
||||
|
||||
const SchemaSelector = ({
|
||||
@@ -48,6 +49,7 @@ const SchemaSelector = ({
|
||||
onSelectSchema,
|
||||
onSelectCreateSchema,
|
||||
portal = true,
|
||||
align = 'start',
|
||||
}: SchemaSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { can: canCreateSchemas } = useAsyncCheckProjectPermissions(
|
||||
@@ -130,7 +132,7 @@ const SchemaSelector = ({
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 min-w-[200px] pointer-events-auto"
|
||||
side="bottom"
|
||||
align="start"
|
||||
align={align}
|
||||
portal={portal}
|
||||
sameWidthAsTrigger
|
||||
>
|
||||
|
||||
@@ -3,14 +3,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useIsInlineEditorEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import Policies from 'components/interfaces/Auth/Policies/Policies'
|
||||
import { Policies } from 'components/interfaces/Auth/Policies/Policies'
|
||||
import { getGeneralPolicyTemplates } from 'components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants'
|
||||
import { PolicyEditorPanel } from 'components/interfaces/Auth/Policies/PolicyEditorPanel'
|
||||
import { generatePolicyUpdateSQL } from 'components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
|
||||
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
|
||||
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 { DocsButton } from 'components/ui/DocsButton'
|
||||
import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel'
|
||||
@@ -24,7 +25,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Input } from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
|
||||
/**
|
||||
* Filter tables by table name and policy name
|
||||
@@ -62,7 +63,6 @@ const onFilterTables = (
|
||||
}
|
||||
|
||||
const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
const { ref } = useParams()
|
||||
const [params, setParams] = useUrlState<{
|
||||
schema?: string
|
||||
search?: string
|
||||
@@ -106,140 +106,144 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="mb-4">
|
||||
<div className="w-full flex flex-col lg:flex-row items-center justify-between gap-2">
|
||||
<ScaffoldContainer size="full">
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div className="mb-4 flex flex-row gap-2 justify-between">
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Filter tables and policies"
|
||||
className="block w-full lg:w-52"
|
||||
containerClassName="[&>div>svg]:-mt-0.5"
|
||||
value={searchString || ''}
|
||||
onChange={(e) => {
|
||||
const str = e.target.value
|
||||
setParams({ ...params, search: str === '' ? undefined : str })
|
||||
}}
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
<SchemaSelector
|
||||
className="w-full lg:w-[180px]"
|
||||
size="tiny"
|
||||
align="end"
|
||||
showError={false}
|
||||
selectedSchemaName={schema}
|
||||
onSelectSchema={(schema) => {
|
||||
setParams({ ...params, search: undefined, schema })
|
||||
}}
|
||||
/>
|
||||
<div className="w-full flex-grow flex items-center justify-between gap-2 lg:gap-4">
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Filter tables and policies"
|
||||
className="block w-full lg:w-52"
|
||||
value={searchString || ''}
|
||||
onChange={(e) => {
|
||||
const str = e.target.value
|
||||
setParams({ ...params, search: str === '' ? undefined : str })
|
||||
}}
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
<DocsButton href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||||
|
||||
{isSuccess && (
|
||||
<Policies
|
||||
{isSuccess && (
|
||||
<Policies
|
||||
search={searchString}
|
||||
schema={schema}
|
||||
tables={filteredTables}
|
||||
hasTables={tables.length > 0}
|
||||
isLocked={isSchemaLocked}
|
||||
onSelectCreatePolicy={(table: string) => {
|
||||
setSelectedTable(table)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelOpen(true)
|
||||
} else {
|
||||
setShowPolicyAiEditor(true)
|
||||
}
|
||||
}}
|
||||
onSelectEditPolicy={(policy) => {
|
||||
setSelectedPolicyToEdit(policy)
|
||||
setSelectedTable(undefined)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelOpen(true)
|
||||
} else {
|
||||
setShowPolicyAiEditor(true)
|
||||
}
|
||||
}}
|
||||
onResetSearch={() => setParams({ ...params, search: undefined })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PolicyEditorPanel
|
||||
visible={showPolicyAiEditor}
|
||||
schema={schema}
|
||||
tables={filteredTables}
|
||||
hasTables={tables.length > 0}
|
||||
isLocked={isSchemaLocked}
|
||||
onSelectCreatePolicy={(table: string) => {
|
||||
setSelectedTable(table)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelOpen(true)
|
||||
} else {
|
||||
setShowPolicyAiEditor(true)
|
||||
}
|
||||
}}
|
||||
onSelectEditPolicy={(policy) => {
|
||||
setSelectedPolicyToEdit(policy)
|
||||
searchString={searchString}
|
||||
selectedTable={selectedTable}
|
||||
selectedPolicy={selectedPolicyToEdit}
|
||||
onSelectCancel={() => {
|
||||
setSelectedTable(undefined)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelOpen(true)
|
||||
} else {
|
||||
setShowPolicyAiEditor(true)
|
||||
}
|
||||
setShowPolicyAiEditor(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
}}
|
||||
authContext="database"
|
||||
/>
|
||||
)}
|
||||
|
||||
<PolicyEditorPanel
|
||||
visible={showPolicyAiEditor}
|
||||
schema={schema}
|
||||
searchString={searchString}
|
||||
selectedTable={selectedTable}
|
||||
selectedPolicy={selectedPolicyToEdit}
|
||||
onSelectCancel={() => {
|
||||
setSelectedTable(undefined)
|
||||
setShowPolicyAiEditor(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
}}
|
||||
authContext="database"
|
||||
/>
|
||||
|
||||
<EditorPanel
|
||||
open={editorPanelOpen}
|
||||
onClose={() => {
|
||||
setEditorPanelOpen(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
setSelectedTable(undefined)
|
||||
}}
|
||||
onRunSuccess={() => {
|
||||
setEditorPanelOpen(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
setSelectedTable(undefined)
|
||||
}}
|
||||
initialValue={
|
||||
selectedPolicyToEdit
|
||||
? generatePolicyUpdateSQL(selectedPolicyToEdit)
|
||||
: selectedTable
|
||||
? `create policy "replace_with_policy_name"
|
||||
on ${schema}.${selectedTable}
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
true -- Write your policy condition here
|
||||
);`
|
||||
: ''
|
||||
}
|
||||
label={
|
||||
selectedPolicyToEdit
|
||||
? 'RLS policies are just SQL statements that you can alter'
|
||||
: selectedTable
|
||||
? `Create new RLS policy on "${selectedTable}"`
|
||||
: ''
|
||||
}
|
||||
initialPrompt={
|
||||
selectedPolicyToEdit
|
||||
? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...`
|
||||
: selectedTable
|
||||
? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...`
|
||||
: ''
|
||||
}
|
||||
templates={
|
||||
selectedPolicyToEdit
|
||||
? getGeneralPolicyTemplates(
|
||||
selectedPolicyToEdit.schema,
|
||||
selectedPolicyToEdit.table
|
||||
).map((template) => ({
|
||||
name: template.templateName,
|
||||
description: template.description,
|
||||
content: template.statement,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<EditorPanel
|
||||
open={editorPanelOpen}
|
||||
onClose={() => {
|
||||
setEditorPanelOpen(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
setSelectedTable(undefined)
|
||||
}}
|
||||
onRunSuccess={() => {
|
||||
setEditorPanelOpen(false)
|
||||
setSelectedPolicyToEdit(undefined)
|
||||
setSelectedTable(undefined)
|
||||
}}
|
||||
initialValue={
|
||||
selectedPolicyToEdit
|
||||
? generatePolicyUpdateSQL(selectedPolicyToEdit)
|
||||
: selectedTable
|
||||
? `create policy "replace_with_policy_name"\n on ${schema}.${selectedTable}\n for select\n to authenticated\n using (\n true -- Write your policy condition here\n);`
|
||||
: ''
|
||||
}
|
||||
label={
|
||||
selectedPolicyToEdit
|
||||
? 'RLS policies are just SQL statements that you can alter'
|
||||
: selectedTable
|
||||
? `Create new RLS policy on "${selectedTable}"`
|
||||
: ''
|
||||
}
|
||||
initialPrompt={
|
||||
selectedPolicyToEdit
|
||||
? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...`
|
||||
: selectedTable
|
||||
? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...`
|
||||
: ''
|
||||
}
|
||||
templates={
|
||||
selectedPolicyToEdit
|
||||
? getGeneralPolicyTemplates(
|
||||
selectedPolicyToEdit.schema,
|
||||
selectedPolicyToEdit.table
|
||||
).map((template) => ({
|
||||
name: template.templateName,
|
||||
description: template.description,
|
||||
content: template.statement,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
AuthPoliciesPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>
|
||||
<div className="h-full p-4">{page}</div>
|
||||
<PageLayout
|
||||
title="Policies"
|
||||
subtitle="Manage Row Level Security policies for your tables"
|
||||
secondaryActions={
|
||||
<DocsButton href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security" />
|
||||
}
|
||||
size="large"
|
||||
>
|
||||
{page}
|
||||
</PageLayout>
|
||||
</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user