PolicyRow.tsx190 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { noop } from 'lodash'
3import { Edit, MoreVertical, Trash } from 'lucide-react'
4import {
5 Button,
6 DropdownMenu,
7 DropdownMenuContent,
8 DropdownMenuItem,
9 DropdownMenuSeparator,
10 DropdownMenuTrigger,
11 TableCell,
12 TableRow,
13 Tooltip,
14 TooltipContent,
15 TooltipTrigger,
16} from 'ui'
17
18import { generatePolicyUpdateSQL } from './PolicyTableRow.utils'
19import type { Policy } from './PolicyTableRow.utils'
20import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
21import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
22import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
23import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
24import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
25import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
26import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
27
28interface PolicyRowProps {
29 policy: Policy
30 onSelectEditPolicy: (policy: Policy) => void
31 onSelectDeletePolicy: (policy: Policy) => void
32 isLocked?: boolean
33}
34
35export const PolicyRow = ({
36 policy,
37 isLocked: isLockedDefault = false,
38 onSelectEditPolicy = noop,
39 onSelectDeletePolicy = noop,
40}: PolicyRowProps) => {
41 const aiSnap = useAiAssistantStateSnapshot()
42 const { openSidebar } = useSidebarManagerSnapshot()
43 const { can: canUpdatePolicies } = useAsyncCheckPermissions(
44 PermissionAction.TENANT_SQL_ADMIN_WRITE,
45 'policies'
46 )
47
48 const { data: project } = useSelectedProjectQuery()
49 const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref })
50
51 // override islocked for Realtime messages table
52 const isLocked =
53 policy.schema === 'realtime' && policy.table === 'messages' ? false : isLockedDefault
54
55 // TODO(km): Simple check for roles that allow authenticated access.
56 // In the future, we'll use splinter to return proper warnings for policies that allow anonymous user access.
57 const appliesToAnonymousUsers =
58 authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED &&
59 (policy.roles.includes('authenticated') || policy.roles.includes('public'))
60
61 const displayedRoles = (() => {
62 const rolesWithAnonymous = appliesToAnonymousUsers
63 ? [...policy.roles, 'anonymous sign-ins']
64 : policy.roles
65 return rolesWithAnonymous
66 })()
67
68 return (
69 <TableRow>
70 <TableCell className="w-[40%] truncate">
71 <div className="flex items-center gap-x-2 min-w-0">
72 <Button
73 type="text"
74 className="text-foreground text-sm p-0 hover:bg-transparent w-full truncate justify-start"
75 onClick={() => onSelectEditPolicy(policy)}
76 >
77 {policy.name}
78 </Button>
79 </div>
80 </TableCell>
81 <TableCell className="w-[20%] truncate">
82 <code className="text-code-inline">{policy.command}</code>
83 </TableCell>
84 <TableCell className="w-[30%] truncate">
85 <div className="flex items-center gap-x-1">
86 <div className="text-foreground-lighter text-sm truncate">
87 {displayedRoles.slice(0, 2).map((role, i) => (
88 <span key={`policy-${role}-${i}`}>
89 <code className="text-code-inline">{role}</code>
90 {i < Math.min(displayedRoles.length, 2) - 1 ? ', ' : ' '}
91 </span>
92 ))}
93 </div>
94 {displayedRoles.length > 2 && (
95 <Tooltip>
96 <TooltipTrigger asChild>
97 <div>
98 <span key="policy-etc" className="text-foreground-light text-xs">
99 + {displayedRoles.length - 2} more
100 </span>
101 </div>
102 </TooltipTrigger>
103 <TooltipContent
104 side="bottom"
105 align="center"
106 className="max-w-80 font-mono flex flex-wrap justify-center gap-y-1"
107 >
108 {displayedRoles.slice(2).map((role, i, arr) => (
109 <>
110 <code key={role} className="text-code-inline break-keep!">
111 {role}
112 </code>
113 {i < arr.length - 1 && ', '}
114 </>
115 ))}
116 </TooltipContent>
117 </Tooltip>
118 )}
119 </div>
120 </TableCell>
121 <TableCell className="text-right whitespace-nowrap">
122 {!isLocked && (
123 <DropdownMenu>
124 <DropdownMenuTrigger asChild>
125 <Button
126 type="default"
127 className="px-1.5"
128 icon={<MoreVertical />}
129 data-testid={`policy-${policy.name}-actions-button`}
130 />
131 </DropdownMenuTrigger>
132 <DropdownMenuContent side="bottom" align="end" className="w-52">
133 <DropdownMenuItem className="gap-x-2" onClick={() => onSelectEditPolicy(policy)}>
134 <Edit size={14} />
135 <p>Edit policy</p>
136 </DropdownMenuItem>
137 <DropdownMenuItem
138 className="space-x-2"
139 onClick={() => {
140 const sql = generatePolicyUpdateSQL(policy)
141 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
142 aiSnap.newChat({
143 name: `Update policy ${policy.name}`,
144 sqlSnippets: [sql],
145 initialInput: `Update the policy with name \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
146 suggestions: {
147 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:`,
148 prompts: [
149 {
150 label: 'Improve Policy',
151 description: 'Tell me how I can improve this policy...',
152 },
153 {
154 label: 'Duplicate Policy',
155 description: 'Duplicate this policy for another table...',
156 },
157 {
158 label: 'Add Conditions',
159 description: 'Add extra conditions to this policy...',
160 },
161 ],
162 },
163 })
164 }}
165 >
166 <Edit size={14} />
167 <p>Edit policy with Assistant</p>
168 </DropdownMenuItem>
169 <DropdownMenuSeparator />
170 <DropdownMenuItemTooltip
171 className="gap-x-2"
172 disabled={!canUpdatePolicies}
173 onClick={() => onSelectDeletePolicy(policy)}
174 tooltip={{
175 content: {
176 side: 'left',
177 text: 'You need additional permissions to delete policies',
178 },
179 }}
180 >
181 <Trash size={14} />
182 <p>Delete policy</p>
183 </DropdownMenuItemTooltip>
184 </DropdownMenuContent>
185 </DropdownMenu>
186 )}
187 </TableCell>
188 </TableRow>
189 )
190}