PolicyRow.tsx 7.2 KB

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