import { acceptUntrustedSql, PGPolicy } from '@supabase/pg-meta' import { isEmpty, noop } from 'lodash' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { Modal } from 'ui' import { POLICY_MODAL_VIEWS } from '../Policies.constants' import { DraftPostgresPolicyCreatePayload, DraftPostgresPolicyUpdatePayload, PolicyFormField, PolicyForReview, PostgresPolicyCreatePayload, PostgresPolicyUpdatePayload, } from '../Policies.types' import { createPayloadForCreatePolicy, createPayloadForUpdatePolicy, createSQLPolicy, } from '../Policies.utils' import { PolicyEditor } from '../PolicyEditor' import { PolicyReview } from '../PolicyReview' import PolicySelection from '../PolicySelection' import PolicyTemplates from '../PolicyTemplates' import { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants' import { getGeneralPolicyTemplates } from './PolicyEditorModal.constants' import PolicyEditorModalTitle from './PolicyEditorModalTitle' import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import useLatest from '@/hooks/misc/useLatest' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' // Call only from a user-gesture handler (the Save click). Promotes the draft payload's // `definition`/`check` from `DisplayableSqlFragment` to executable `SafeSqlFragment`. const acceptCreatePayload = ( draft: DraftPostgresPolicyCreatePayload ): PostgresPolicyCreatePayload => ({ ...draft, definition: draft.definition === undefined ? undefined : acceptUntrustedSql(draft.definition), check: draft.check === undefined ? undefined : acceptUntrustedSql(draft.check), }) const acceptUpdatePayload = ( draft: DraftPostgresPolicyUpdatePayload ): PostgresPolicyUpdatePayload => ({ ...draft, definition: draft.definition === undefined ? undefined : acceptUntrustedSql(draft.definition), check: draft.check === undefined ? undefined : acceptUntrustedSql(draft.check), }) interface PolicyEditorModalProps { visible?: boolean schema?: string table?: string selectedPolicyToEdit?: PGPolicy showAssistantPreview?: boolean onSelectCancel: () => void onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => Promise onSaveSuccess: () => void } export const PolicyEditorModal = ({ visible = false, schema = '', table = '', selectedPolicyToEdit, showAssistantPreview = false, onSelectCancel = noop, onCreatePolicy, onUpdatePolicy, onSaveSuccess = noop, }: PolicyEditorModalProps) => { const { toggleFeaturePreviewModal } = useFeaturePreviewModal() const newPolicyTemplate: PolicyFormField = { schema, table, name: '', definition: '', check: '', command: null, roles: [], } const isNewPolicy = isEmpty(selectedPolicyToEdit) const initializedPolicyFormFields = isNewPolicy ? newPolicyTemplate : selectedPolicyToEdit // Mainly to decide which view to show when back from templates const [previousView, setPreviousView] = useState('') const [view, setView] = useState(POLICY_MODAL_VIEWS.EDITOR) const [policyFormFields, setPolicyFormFields] = useState( initializedPolicyFormFields ) const [policyStatementForReview, setPolicyStatementForReview] = useState() const [isDirty, setIsDirty] = useState(false) const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose: () => { onSelectCancel() setIsDirty(false) }, }) const onViewIntro = useCallback(() => setView(POLICY_MODAL_VIEWS.SELECTION), []) const onViewEditor = useCallback(() => setView(POLICY_MODAL_VIEWS.EDITOR), []) const onViewTemplates = () => { setPreviousView(view) setView(POLICY_MODAL_VIEWS.TEMPLATES) } const onReviewPolicy = () => setView(POLICY_MODAL_VIEWS.REVIEW) const onSelectBackFromTemplates = () => setView(previousView) const isNewPolicyRef = useLatest(isNewPolicy) const initializedPolicyFormFieldsRef = useLatest(initializedPolicyFormFields) useEffect(() => { if (visible) { if (isNewPolicyRef.current) { onViewIntro() } else { onViewEditor() } setPolicyFormFields(initializedPolicyFormFieldsRef.current) } }, [ onViewIntro, onViewEditor, isNewPolicyRef, initializedPolicyFormFieldsRef, // end of stable references visible, ]) /* Methods that are for the UI */ const onToggleFeaturePreviewModal = () => { toggleFeaturePreviewModal(true) onSelectCancel() } const onUseTemplate = (template: PolicyTemplate) => { setPolicyFormFields({ ...policyFormFields, name: template.name, definition: template.definition, check: template.check, command: template.command, roles: template.roles, }) onViewEditor() } const onUpdatePolicyFormFields = (field: Partial) => { setIsDirty(true) if (field.name && field.name.length > 63) return setPolicyFormFields({ ...policyFormFields, ...field }) } const validatePolicyFormFields = () => { const { name, definition, check, command } = policyFormFields if (name.length === 0) { return toast.error('Please provide a name for your policy') } if (!command) { return toast.error('Please select an operation for your policy') } if (['SELECT', 'DELETE'].includes(command) && !definition) { return toast.error('Please provide a USING expression for your policy') } if (command === 'INSERT' && !check) { return toast.error('Please provide a WITH CHECK expression for your policy') } if (command === 'UPDATE' && !definition && !check) { return toast.error( 'Please provide either a USING, or WITH CHECK expression, or both for your policy' ) } const policySQLStatement = createSQLPolicy(policyFormFields, selectedPolicyToEdit) setPolicyStatementForReview(policySQLStatement) onReviewPolicy() } // The Save click is the explicit user gesture that promotes editor SQL to executable. const onReviewSave = () => { const payload = isNewPolicy ? acceptCreatePayload(createPayloadForCreatePolicy(policyFormFields)) : acceptUpdatePayload(createPayloadForUpdatePolicy(policyFormFields, selectedPolicyToEdit)) onSavePolicy(payload) setIsDirty(false) } const onSavePolicy = async ( payload: PostgresPolicyCreatePayload | PostgresPolicyUpdatePayload ) => { // @ts-ignore const hasError = isNewPolicy ? await onCreatePolicy(payload) : await onUpdatePolicy(payload) hasError ? onViewEditor() : onSaveSuccess() } return ( , ]} onCancel={confirmOnClose} >
{view === POLICY_MODAL_VIEWS.SELECTION ? ( ) : view === POLICY_MODAL_VIEWS.EDITOR ? ( ) : view === POLICY_MODAL_VIEWS.TEMPLATES ? ( !policy.preview)} templatesNote="* References a specific column in the table" onUseTemplate={onUseTemplate} /> ) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? ( ) : null}
) }