| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- 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<boolean>
- onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => Promise<boolean>
- 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<PolicyFormField>(
- initializedPolicyFormFields
- )
- const [policyStatementForReview, setPolicyStatementForReview] = useState<PolicyForReview>()
- 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<PolicyFormField>) => {
- 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 (
- <Modal
- hideFooter
- size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
- visible={visible}
- contentStyle={{ padding: 0 }}
- header={[
- <PolicyEditorModalTitle
- key="0"
- view={view}
- isNewPolicy={isNewPolicy}
- schema={schema}
- table={table}
- showAssistantPreview={showAssistantPreview}
- onSelectBackFromTemplates={onSelectBackFromTemplates}
- onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
- />,
- ]}
- onCancel={confirmOnClose}
- >
- <div>
- <DiscardChangesConfirmationDialog {...modalProps} />
- {view === POLICY_MODAL_VIEWS.SELECTION ? (
- <PolicySelection
- description="Write rules with PostgreSQL's policies to fit your unique business needs."
- onViewTemplates={onViewTemplates}
- onViewEditor={onViewEditor}
- showAssistantPreview={showAssistantPreview}
- onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
- />
- ) : view === POLICY_MODAL_VIEWS.EDITOR ? (
- <PolicyEditor
- isNewPolicy={isNewPolicy}
- policyFormFields={policyFormFields}
- onUpdatePolicyFormFields={onUpdatePolicyFormFields}
- onViewTemplates={onViewTemplates}
- onReviewPolicy={validatePolicyFormFields}
- />
- ) : view === POLICY_MODAL_VIEWS.TEMPLATES ? (
- <PolicyTemplates
- templates={getGeneralPolicyTemplates(schema, table).filter((policy) => !policy.preview)}
- templatesNote="* References a specific column in the table"
- onUseTemplate={onUseTemplate}
- />
- ) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? (
- <PolicyReview
- policy={policyStatementForReview}
- onSelectBack={onViewEditor}
- onSelectSave={onReviewSave}
- />
- ) : null}
- </div>
- </Modal>
- )
- }
|