index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { acceptUntrustedSql, PGPolicy } from '@supabase/pg-meta'
  2. import { isEmpty, noop } from 'lodash'
  3. import { useCallback, useEffect, useState } from 'react'
  4. import { toast } from 'sonner'
  5. import { Modal } from 'ui'
  6. import { POLICY_MODAL_VIEWS } from '../Policies.constants'
  7. import {
  8. DraftPostgresPolicyCreatePayload,
  9. DraftPostgresPolicyUpdatePayload,
  10. PolicyFormField,
  11. PolicyForReview,
  12. PostgresPolicyCreatePayload,
  13. PostgresPolicyUpdatePayload,
  14. } from '../Policies.types'
  15. import {
  16. createPayloadForCreatePolicy,
  17. createPayloadForUpdatePolicy,
  18. createSQLPolicy,
  19. } from '../Policies.utils'
  20. import { PolicyEditor } from '../PolicyEditor'
  21. import { PolicyReview } from '../PolicyReview'
  22. import PolicySelection from '../PolicySelection'
  23. import PolicyTemplates from '../PolicyTemplates'
  24. import { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants'
  25. import { getGeneralPolicyTemplates } from './PolicyEditorModal.constants'
  26. import PolicyEditorModalTitle from './PolicyEditorModalTitle'
  27. import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
  28. import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
  29. import useLatest from '@/hooks/misc/useLatest'
  30. import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
  31. // Call only from a user-gesture handler (the Save click). Promotes the draft payload's
  32. // `definition`/`check` from `DisplayableSqlFragment` to executable `SafeSqlFragment`.
  33. const acceptCreatePayload = (
  34. draft: DraftPostgresPolicyCreatePayload
  35. ): PostgresPolicyCreatePayload => ({
  36. ...draft,
  37. definition: draft.definition === undefined ? undefined : acceptUntrustedSql(draft.definition),
  38. check: draft.check === undefined ? undefined : acceptUntrustedSql(draft.check),
  39. })
  40. const acceptUpdatePayload = (
  41. draft: DraftPostgresPolicyUpdatePayload
  42. ): PostgresPolicyUpdatePayload => ({
  43. ...draft,
  44. definition: draft.definition === undefined ? undefined : acceptUntrustedSql(draft.definition),
  45. check: draft.check === undefined ? undefined : acceptUntrustedSql(draft.check),
  46. })
  47. interface PolicyEditorModalProps {
  48. visible?: boolean
  49. schema?: string
  50. table?: string
  51. selectedPolicyToEdit?: PGPolicy
  52. showAssistantPreview?: boolean
  53. onSelectCancel: () => void
  54. onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise<boolean>
  55. onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => Promise<boolean>
  56. onSaveSuccess: () => void
  57. }
  58. export const PolicyEditorModal = ({
  59. visible = false,
  60. schema = '',
  61. table = '',
  62. selectedPolicyToEdit,
  63. showAssistantPreview = false,
  64. onSelectCancel = noop,
  65. onCreatePolicy,
  66. onUpdatePolicy,
  67. onSaveSuccess = noop,
  68. }: PolicyEditorModalProps) => {
  69. const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
  70. const newPolicyTemplate: PolicyFormField = {
  71. schema,
  72. table,
  73. name: '',
  74. definition: '',
  75. check: '',
  76. command: null,
  77. roles: [],
  78. }
  79. const isNewPolicy = isEmpty(selectedPolicyToEdit)
  80. const initializedPolicyFormFields = isNewPolicy ? newPolicyTemplate : selectedPolicyToEdit
  81. // Mainly to decide which view to show when back from templates
  82. const [previousView, setPreviousView] = useState('')
  83. const [view, setView] = useState(POLICY_MODAL_VIEWS.EDITOR)
  84. const [policyFormFields, setPolicyFormFields] = useState<PolicyFormField>(
  85. initializedPolicyFormFields
  86. )
  87. const [policyStatementForReview, setPolicyStatementForReview] = useState<PolicyForReview>()
  88. const [isDirty, setIsDirty] = useState(false)
  89. const { confirmOnClose, modalProps } = useConfirmOnClose({
  90. checkIsDirty: () => isDirty,
  91. onClose: () => {
  92. onSelectCancel()
  93. setIsDirty(false)
  94. },
  95. })
  96. const onViewIntro = useCallback(() => setView(POLICY_MODAL_VIEWS.SELECTION), [])
  97. const onViewEditor = useCallback(() => setView(POLICY_MODAL_VIEWS.EDITOR), [])
  98. const onViewTemplates = () => {
  99. setPreviousView(view)
  100. setView(POLICY_MODAL_VIEWS.TEMPLATES)
  101. }
  102. const onReviewPolicy = () => setView(POLICY_MODAL_VIEWS.REVIEW)
  103. const onSelectBackFromTemplates = () => setView(previousView)
  104. const isNewPolicyRef = useLatest(isNewPolicy)
  105. const initializedPolicyFormFieldsRef = useLatest(initializedPolicyFormFields)
  106. useEffect(() => {
  107. if (visible) {
  108. if (isNewPolicyRef.current) {
  109. onViewIntro()
  110. } else {
  111. onViewEditor()
  112. }
  113. setPolicyFormFields(initializedPolicyFormFieldsRef.current)
  114. }
  115. }, [
  116. onViewIntro,
  117. onViewEditor,
  118. isNewPolicyRef,
  119. initializedPolicyFormFieldsRef,
  120. // end of stable references
  121. visible,
  122. ])
  123. /* Methods that are for the UI */
  124. const onToggleFeaturePreviewModal = () => {
  125. toggleFeaturePreviewModal(true)
  126. onSelectCancel()
  127. }
  128. const onUseTemplate = (template: PolicyTemplate) => {
  129. setPolicyFormFields({
  130. ...policyFormFields,
  131. name: template.name,
  132. definition: template.definition,
  133. check: template.check,
  134. command: template.command,
  135. roles: template.roles,
  136. })
  137. onViewEditor()
  138. }
  139. const onUpdatePolicyFormFields = (field: Partial<PolicyFormField>) => {
  140. setIsDirty(true)
  141. if (field.name && field.name.length > 63) return
  142. setPolicyFormFields({ ...policyFormFields, ...field })
  143. }
  144. const validatePolicyFormFields = () => {
  145. const { name, definition, check, command } = policyFormFields
  146. if (name.length === 0) {
  147. return toast.error('Please provide a name for your policy')
  148. }
  149. if (!command) {
  150. return toast.error('Please select an operation for your policy')
  151. }
  152. if (['SELECT', 'DELETE'].includes(command) && !definition) {
  153. return toast.error('Please provide a USING expression for your policy')
  154. }
  155. if (command === 'INSERT' && !check) {
  156. return toast.error('Please provide a WITH CHECK expression for your policy')
  157. }
  158. if (command === 'UPDATE' && !definition && !check) {
  159. return toast.error(
  160. 'Please provide either a USING, or WITH CHECK expression, or both for your policy'
  161. )
  162. }
  163. const policySQLStatement = createSQLPolicy(policyFormFields, selectedPolicyToEdit)
  164. setPolicyStatementForReview(policySQLStatement)
  165. onReviewPolicy()
  166. }
  167. // The Save click is the explicit user gesture that promotes editor SQL to executable.
  168. const onReviewSave = () => {
  169. const payload = isNewPolicy
  170. ? acceptCreatePayload(createPayloadForCreatePolicy(policyFormFields))
  171. : acceptUpdatePayload(createPayloadForUpdatePolicy(policyFormFields, selectedPolicyToEdit))
  172. onSavePolicy(payload)
  173. setIsDirty(false)
  174. }
  175. const onSavePolicy = async (
  176. payload: PostgresPolicyCreatePayload | PostgresPolicyUpdatePayload
  177. ) => {
  178. // @ts-ignore
  179. const hasError = isNewPolicy ? await onCreatePolicy(payload) : await onUpdatePolicy(payload)
  180. hasError ? onViewEditor() : onSaveSuccess()
  181. }
  182. return (
  183. <Modal
  184. hideFooter
  185. size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
  186. visible={visible}
  187. contentStyle={{ padding: 0 }}
  188. header={[
  189. <PolicyEditorModalTitle
  190. key="0"
  191. view={view}
  192. isNewPolicy={isNewPolicy}
  193. schema={schema}
  194. table={table}
  195. showAssistantPreview={showAssistantPreview}
  196. onSelectBackFromTemplates={onSelectBackFromTemplates}
  197. onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
  198. />,
  199. ]}
  200. onCancel={confirmOnClose}
  201. >
  202. <div>
  203. <DiscardChangesConfirmationDialog {...modalProps} />
  204. {view === POLICY_MODAL_VIEWS.SELECTION ? (
  205. <PolicySelection
  206. description="Write rules with PostgreSQL's policies to fit your unique business needs."
  207. onViewTemplates={onViewTemplates}
  208. onViewEditor={onViewEditor}
  209. showAssistantPreview={showAssistantPreview}
  210. onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
  211. />
  212. ) : view === POLICY_MODAL_VIEWS.EDITOR ? (
  213. <PolicyEditor
  214. isNewPolicy={isNewPolicy}
  215. policyFormFields={policyFormFields}
  216. onUpdatePolicyFormFields={onUpdatePolicyFormFields}
  217. onViewTemplates={onViewTemplates}
  218. onReviewPolicy={validatePolicyFormFields}
  219. />
  220. ) : view === POLICY_MODAL_VIEWS.TEMPLATES ? (
  221. <PolicyTemplates
  222. templates={getGeneralPolicyTemplates(schema, table).filter((policy) => !policy.preview)}
  223. templatesNote="* References a specific column in the table"
  224. onUseTemplate={onUseTemplate}
  225. />
  226. ) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? (
  227. <PolicyReview
  228. policy={policyStatementForReview}
  229. onSelectBack={onViewEditor}
  230. onSelectSave={onReviewSave}
  231. />
  232. ) : null}
  233. </div>
  234. </Modal>
  235. )
  236. }