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