Policies.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { useParams } from 'common'
  2. import { isEmpty } from 'lodash'
  3. import Link from 'next/link'
  4. import { useCallback, useState } from 'react'
  5. import { toast } from 'sonner'
  6. import { Button, Card, CardContent } from 'ui'
  7. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  8. import {
  9. PolicyTableRow,
  10. PolicyTableRowProps,
  11. } from '@/components/interfaces/Auth/Policies/PolicyTableRow'
  12. import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
  13. import { ProtectedSchemaWarning } from '@/components/interfaces/Database/ProtectedSchemaWarning'
  14. import { NoSearchResults } from '@/components/ui/NoSearchResults'
  15. import { useDatabasePolicyDeleteMutation } from '@/data/database-policies/database-policy-delete-mutation'
  16. import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
  17. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  18. interface PoliciesProps {
  19. search?: string
  20. schema: string
  21. tables: PolicyTableRowProps['table'][]
  22. hasTables: boolean
  23. isLocked: boolean
  24. visibleTableIds: Set<number>
  25. onSelectCreatePolicy: (table: string) => void
  26. onSelectEditPolicy: (policy: Policy) => void
  27. onResetSearch?: () => void
  28. }
  29. export const Policies = ({
  30. search,
  31. schema,
  32. tables,
  33. hasTables,
  34. isLocked,
  35. visibleTableIds,
  36. onSelectCreatePolicy,
  37. onSelectEditPolicy: onSelectEditPolicyAI,
  38. onResetSearch,
  39. }: PoliciesProps) => {
  40. const { ref } = useParams()
  41. const { data: project } = useSelectedProjectQuery()
  42. const [selectedTableToToggleRLS, setSelectedTableToToggleRLS] = useState<{
  43. id: number
  44. schema: string
  45. name: string
  46. rls_enabled: boolean
  47. }>()
  48. const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<any>({})
  49. const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
  50. onError: (error) => {
  51. toast.error(`Failed to toggle RLS: ${error.message}`)
  52. },
  53. onSettled: () => {
  54. closeConfirmModal()
  55. },
  56. })
  57. const { mutate: deleteDatabasePolicy, isPending: isDeletingPolicy } =
  58. useDatabasePolicyDeleteMutation({
  59. onSuccess: () => {
  60. toast.success('Successfully deleted policy!')
  61. },
  62. onSettled: () => {
  63. closeConfirmModal()
  64. },
  65. })
  66. const closeConfirmModal = useCallback(() => {
  67. setSelectedPolicyToDelete({})
  68. setSelectedTableToToggleRLS(undefined)
  69. }, [])
  70. const onSelectToggleRLS = useCallback(
  71. (table: { id: number; schema: string; name: string; rls_enabled: boolean }) => {
  72. setSelectedTableToToggleRLS(table)
  73. },
  74. []
  75. )
  76. const onSelectEditPolicy = useCallback(
  77. (policy: Policy) => {
  78. onSelectEditPolicyAI(policy)
  79. },
  80. [onSelectEditPolicyAI]
  81. )
  82. const onSelectDeletePolicy = useCallback((policy: Policy) => {
  83. setSelectedPolicyToDelete(policy)
  84. }, [])
  85. // Methods that involve some API
  86. const onToggleRLS = async () => {
  87. if (!selectedTableToToggleRLS) return console.error('Table is required')
  88. const payload = {
  89. id: selectedTableToToggleRLS.id,
  90. rls_enabled: !selectedTableToToggleRLS.rls_enabled,
  91. }
  92. updateTable({
  93. projectRef: project?.ref!,
  94. connectionString: project?.connectionString,
  95. id: selectedTableToToggleRLS.id,
  96. name: selectedTableToToggleRLS.name,
  97. schema: selectedTableToToggleRLS.schema,
  98. payload: payload,
  99. })
  100. }
  101. const onDeletePolicy = async () => {
  102. if (!project) return console.error('Project is required')
  103. deleteDatabasePolicy({
  104. projectRef: project.ref,
  105. connectionString: project.connectionString,
  106. originalPolicy: selectedPolicyToDelete,
  107. })
  108. }
  109. const handleCreatePolicy = useCallback(
  110. (tableData: PolicyTableRowProps['table']) => {
  111. onSelectCreatePolicy(tableData.name)
  112. },
  113. [onSelectCreatePolicy]
  114. )
  115. if (!hasTables) {
  116. return (
  117. <Card className="w-full bg-transparent">
  118. <CardContent className="flex flex-col items-center justify-center p-8">
  119. <h2 className="heading-default">No tables to create policies for</h2>
  120. <p className="text-sm text-foreground-light text-center mb-4">
  121. RLS Policies control per-user access to table rows. Create a table in this schema first
  122. before creating a policy.
  123. </p>
  124. <Button asChild type="default">
  125. <Link href={`/project/${ref}/editor`}>Create a table</Link>
  126. </Button>
  127. </CardContent>
  128. </Card>
  129. )
  130. }
  131. return (
  132. <>
  133. <div className="flex flex-col gap-y-4 pb-4">
  134. {isLocked && <ProtectedSchemaWarning schema={schema} entity="policies" />}
  135. {tables.length > 0 ? (
  136. <>
  137. {tables.map((table) => {
  138. const isVisible = visibleTableIds.has(table.id)
  139. return (
  140. <section
  141. key={table.id}
  142. hidden={!isVisible}
  143. aria-hidden={!isVisible}
  144. data-testid={`policy-table-${table.name}`}
  145. >
  146. <PolicyTableRow
  147. table={table}
  148. isLocked={schema === 'realtime' ? true : isLocked}
  149. onSelectToggleRLS={onSelectToggleRLS}
  150. onSelectCreatePolicy={handleCreatePolicy}
  151. onSelectEditPolicy={onSelectEditPolicy}
  152. onSelectDeletePolicy={onSelectDeletePolicy}
  153. />
  154. </section>
  155. )
  156. })}
  157. {!!search && visibleTableIds.size === 0 && (
  158. <NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
  159. )}
  160. </>
  161. ) : hasTables ? (
  162. <NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
  163. ) : null}
  164. </div>
  165. <ConfirmationModal
  166. visible={!isEmpty(selectedPolicyToDelete)}
  167. variant="destructive"
  168. title="Delete policy"
  169. description={`Are you sure you want to delete the policy “${selectedPolicyToDelete.name}”? This action cannot be undone.`}
  170. confirmLabel="Delete"
  171. confirmLabelLoading="Deleting"
  172. loading={isDeletingPolicy}
  173. onCancel={closeConfirmModal}
  174. onConfirm={onDeletePolicy}
  175. />
  176. <ConfirmationModal
  177. visible={selectedTableToToggleRLS !== undefined}
  178. variant={selectedTableToToggleRLS?.rls_enabled ? 'destructive' : 'default'}
  179. title={`${selectedTableToToggleRLS?.rls_enabled ? 'Disable' : 'Enable'} Row Level Security`}
  180. description={`Are you sure you want to ${
  181. selectedTableToToggleRLS?.rls_enabled ? 'disable' : 'enable'
  182. } Row Level Security (RLS) for the table “${selectedTableToToggleRLS?.name}”?`}
  183. confirmLabel={`${selectedTableToToggleRLS?.rls_enabled ? 'Disable' : 'Enable'} RLS`}
  184. confirmLabelLoading={`${selectedTableToToggleRLS?.rls_enabled ? 'Disabling' : 'Enabling'} RLS`}
  185. loading={isUpdatingTable}
  186. onCancel={closeConfirmModal}
  187. onConfirm={onToggleRLS}
  188. />
  189. </>
  190. )
  191. }