mirror of
https://github.com/supabase/supabase.git
synced 2026-06-04 11:51:55 +08:00
refactor(close confirmation modal): abstract out useConfirmOnClose hook (#40310)
Abstract out a hook, useConfirmOnClose, for the pattern where we show a confirmation modal on close, conditional on whether the user has made edits. --------- Co-authored-by: Alaister Young <a@alaisteryoung.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { isEmpty, noop } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Modal } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
|
||||
@@ -71,22 +73,17 @@ const PolicyEditorModal = ({
|
||||
)
|
||||
const [policyStatementForReview, setPolicyStatementForReview] = useState<any>('')
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const [isClosingPolicyEditorModal, setIsClosingPolicyEditorModal] = useState(false)
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (isNewPolicy) {
|
||||
onViewIntro()
|
||||
} else {
|
||||
onViewEditor()
|
||||
}
|
||||
setPolicyFormFields(initializedPolicyFormFields)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
/* Methods that are for the UI */
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
onSelectCancel()
|
||||
setIsDirty(false)
|
||||
},
|
||||
})
|
||||
|
||||
const onViewIntro = () => setView(POLICY_MODAL_VIEWS.SELECTION)
|
||||
const onViewEditor = () => setView(POLICY_MODAL_VIEWS.EDITOR)
|
||||
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)
|
||||
@@ -94,6 +91,28 @@ const PolicyEditorModal = ({
|
||||
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()
|
||||
onSelectCancel()
|
||||
@@ -158,10 +177,6 @@ const PolicyEditorModal = ({
|
||||
hasError ? onViewEditor() : onSaveSuccess()
|
||||
}
|
||||
|
||||
const isClosingPolicyEditor = () => {
|
||||
isDirty ? setIsClosingPolicyEditorModal(true) : onSelectCancel()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideFooter
|
||||
@@ -180,25 +195,10 @@ const PolicyEditorModal = ({
|
||||
onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
|
||||
/>,
|
||||
]}
|
||||
onCancel={isClosingPolicyEditor}
|
||||
onCancel={confirmOnClose}
|
||||
>
|
||||
<div>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPolicyEditorModal}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPolicyEditorModal(false)}
|
||||
onConfirm={() => {
|
||||
onSelectCancel()
|
||||
setIsClosingPolicyEditorModal(false)
|
||||
setIsDirty(false)
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the editor? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
{view === POLICY_MODAL_VIEWS.SELECTION ? (
|
||||
<PolicySelection
|
||||
description="Write rules with PostgreSQL's policies to fit your unique business needs."
|
||||
@@ -233,4 +233,19 @@ const PolicyEditorModal = ({
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the editor? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
export default PolicyEditorModal
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { isEqual } from 'lodash'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
@@ -17,6 +17,7 @@ import { databasePoliciesKeys } from 'data/database-policies/keys'
|
||||
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -94,7 +95,6 @@ export const PolicyEditorPanel = memo(function ({
|
||||
const [selectedDiff, setSelectedDiff] = useState<string>()
|
||||
|
||||
const [showTools, setShowTools] = useState<boolean>(false)
|
||||
const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState<boolean>(false)
|
||||
|
||||
const formId = 'rls-editor'
|
||||
const FormSchema = z.object({
|
||||
@@ -139,7 +139,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
},
|
||||
})
|
||||
|
||||
const onClosingPanel = () => {
|
||||
const hasUnsavedChanges = useCallback(() => {
|
||||
const editorOneValue = editorOneRef.current?.getValue().trim() ?? null
|
||||
const editorOneFormattedValue = !editorOneValue ? null : editorOneValue
|
||||
const editorTwoValue = editorTwoRef.current?.getValue().trim() ?? null
|
||||
@@ -147,7 +147,10 @@ export const PolicyEditorPanel = memo(function ({
|
||||
|
||||
const policyCreateUnsaved =
|
||||
selectedPolicy === undefined &&
|
||||
(name.length > 0 || roles.length > 0 || editorOneFormattedValue || editorTwoFormattedValue)
|
||||
(name.length > 0 ||
|
||||
roles.length > 0 ||
|
||||
!!editorOneFormattedValue ||
|
||||
!!editorTwoFormattedValue)
|
||||
const policyUpdateUnsaved =
|
||||
selectedPolicy !== undefined
|
||||
? checkIfPolicyHasChanged(selectedPolicy, {
|
||||
@@ -158,12 +161,13 @@ export const PolicyEditorPanel = memo(function ({
|
||||
})
|
||||
: false
|
||||
|
||||
if (policyCreateUnsaved || policyUpdateUnsaved) {
|
||||
setIsClosingPolicyEditorPanel(true)
|
||||
} else {
|
||||
onSelectCancel()
|
||||
}
|
||||
}
|
||||
return policyCreateUnsaved || policyUpdateUnsaved
|
||||
}, [command, name, roles, selectedPolicy])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: hasUnsavedChanges,
|
||||
onClose: onSelectCancel,
|
||||
})
|
||||
|
||||
const onSubmit = (data: z.infer<typeof FormSchema>) => {
|
||||
const { name, table, behavior, command, roles } = data
|
||||
@@ -242,7 +246,6 @@ export const PolicyEditorPanel = memo(function ({
|
||||
editorOneRef.current?.setValue('')
|
||||
editorTwoRef.current?.setValue('')
|
||||
setShowTools(false)
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
setError(undefined)
|
||||
setShowDetails(false)
|
||||
setSelectedDiff(undefined)
|
||||
@@ -288,7 +291,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
<>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Sheet open={visible} onOpenChange={() => onClosingPanel()}>
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={showTools ? 'lg' : 'default'}
|
||||
@@ -481,7 +484,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isExecuting || isUpdating}
|
||||
onClick={() => onClosingPanel()}
|
||||
onClick={confirmOnClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -569,23 +572,24 @@ export const PolicyEditorPanel = memo(function ({
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isClosingPolicyEditorPanel}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPolicyEditorPanel(false)}
|
||||
onConfirm={() => {
|
||||
onSelectCancel()
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
PolicyEditorPanel.displayName = 'PolicyEditorPanel'
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useDatabaseFunctionCreateMutation } from 'data/database-functions/datab
|
||||
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import type { FormSchema } from 'types'
|
||||
import {
|
||||
@@ -76,7 +77,6 @@ export const CreateFunction = ({
|
||||
onClose,
|
||||
}: CreateFunctionProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [isClosingPanel, setIsClosingPanel] = useState(false)
|
||||
const [advancedSettingsShown, setAdvancedSettingsShown] = useState(false)
|
||||
const [focusedEditor, setFocusedEditor] = useState(false)
|
||||
|
||||
@@ -87,15 +87,16 @@ export const CreateFunction = ({
|
||||
})
|
||||
const language = form.watch('language')
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const { mutate: createDatabaseFunction, isLoading: isCreating } =
|
||||
useDatabaseFunctionCreateMutation()
|
||||
const { mutate: updateDatabaseFunction, isLoading: isUpdating } =
|
||||
useDatabaseFunctionUpdateMutation()
|
||||
|
||||
function isClosingSidePanel() {
|
||||
form.formState.isDirty ? setIsClosingPanel(true) : onClose()
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
const payload = {
|
||||
@@ -156,7 +157,7 @@ export const CreateFunction = ({
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={() => isClosingSidePanel()}>
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={'default'}
|
||||
@@ -388,7 +389,7 @@ export const CreateFunction = ({
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<SheetFooter>
|
||||
<Button disabled={isCreating || isUpdating} type="default" onClick={isClosingSidePanel}>
|
||||
<Button disabled={isCreating || isUpdating} type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -401,26 +402,27 @@ export const CreateFunction = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
interface FormFieldConfigParamsProps {
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/databas
|
||||
import { getTableEditor } from 'data/table-editor/table-editor-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { isValidHttpUrl, uuidv4 } from 'lib/helpers'
|
||||
import { Button, Form, SidePanel } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
@@ -38,7 +39,6 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP
|
||||
const { ref } = useParams()
|
||||
const submitRef = useRef<any>(null)
|
||||
const [isEdited, setIsEdited] = useState(false)
|
||||
const [isClosingPanel, setIsClosingPanel] = useState(false)
|
||||
|
||||
// [Joshen] There seems to be some bug between Checkbox.Group within the Form component
|
||||
// hence why this external state as a temporary workaround
|
||||
@@ -135,11 +135,6 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP
|
||||
timeout_ms: Number(selectedHook?.function_args?.[4] ?? 5000),
|
||||
}
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (isEdited) setIsClosingPanel(true)
|
||||
else onClose()
|
||||
}
|
||||
|
||||
const onUpdateSelectedEvents = (event: string) => {
|
||||
if (events.includes(event)) {
|
||||
setEvents(events.filter((e) => e !== event))
|
||||
@@ -261,10 +256,17 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setIsEdited(false)
|
||||
setIsClosingPanel(false)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isEdited,
|
||||
onClose: () => {
|
||||
setIsEdited(false)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanel
|
||||
@@ -281,14 +283,14 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP
|
||||
}
|
||||
className="hooks-sidepanel mr-0 transform transition-all duration-300 ease-in-out"
|
||||
onConfirm={() => {}}
|
||||
onCancel={() => onClosePanel()}
|
||||
onCancel={confirmOnClose}
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-default px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
onClick={confirmOnClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
@@ -328,22 +330,22 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP
|
||||
}}
|
||||
</Form>
|
||||
</SidePanel>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
setIsEdited(false)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/databas
|
||||
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import ChooseFunctionForm from './ChooseFunctionForm'
|
||||
import {
|
||||
@@ -40,7 +42,6 @@ import {
|
||||
TRIGGER_ORIENTATIONS,
|
||||
TRIGGER_TYPES,
|
||||
} from './Triggers.constants'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
const formId = 'create-trigger'
|
||||
|
||||
@@ -89,7 +90,6 @@ export const TriggerSheet = ({
|
||||
}: TriggerSheetProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const [isClosingPanel, setIsClosingPanel] = useState(false)
|
||||
const [showFunctionSelector, setShowFunctionSelector] = useState(false)
|
||||
|
||||
const { mutate: createDatabaseTrigger, isLoading: isCreating } = useDatabaseTriggerCreateMutation(
|
||||
@@ -135,9 +135,10 @@ export const TriggerSheet = ({
|
||||
})
|
||||
const { function_name, function_schema } = form.watch()
|
||||
|
||||
function isClosingSidePanel() {
|
||||
form.formState.isDirty ? setIsClosingPanel(true) : onClose()
|
||||
}
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
@@ -188,7 +189,7 @@ export const TriggerSheet = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={isClosingSidePanel}>
|
||||
<Sheet open={open} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="lg" className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
@@ -471,7 +472,7 @@ export const TriggerSheet = ({
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
disabled={isCreating || isUpdating}
|
||||
onClick={onClose}
|
||||
onClick={confirmOnClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -480,21 +481,7 @@ export const TriggerSheet = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -509,3 +496,18 @@ export const TriggerSheet = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { SubmitHandler, useForm, type UseFormReturn } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
@@ -7,6 +7,7 @@ import z from 'zod'
|
||||
import { useParams } from 'common'
|
||||
import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation'
|
||||
import { ProjectSecret } from 'data/secrets/secrets-query'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Eye, EyeOff, X } from 'lucide-react'
|
||||
import { useLatest } from 'react-use'
|
||||
import {
|
||||
@@ -73,7 +74,7 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro
|
||||
})
|
||||
}
|
||||
|
||||
const { confirmOnClose, modal: closeConfirmationModal } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
@@ -97,7 +98,7 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
{closeConfirmationModal}
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -211,58 +212,11 @@ const SecretField = ({ form }: SecretFieldProps): ReactNode => {
|
||||
)
|
||||
}
|
||||
|
||||
type UseConfirmOnCloseParams = {
|
||||
checkIsDirty: () => boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type ConfirmOnCloseReturn = {
|
||||
confirmOnClose: () => void
|
||||
modal: ReactNode
|
||||
}
|
||||
|
||||
const useConfirmOnClose = ({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
}: UseConfirmOnCloseParams): ConfirmOnCloseReturn => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const confirmOnClose = useCallback(() => {
|
||||
if (checkIsDirty()) {
|
||||
setVisible(true)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}, [checkIsDirty, onClose])
|
||||
|
||||
const onConfirm = () => {
|
||||
setVisible(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return {
|
||||
confirmOnClose,
|
||||
modal: (
|
||||
<CloseConfirmationModal
|
||||
visible={visible}
|
||||
onClose={onConfirm}
|
||||
onCancel={() => setVisible(false)}
|
||||
/>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type CloseConfirmationModalProps = {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
onCancel,
|
||||
}: CloseConfirmationModalProps): ReactNode => {
|
||||
}: ConfirmOnCloseModalProps): ReactNode => {
|
||||
return (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { CRONJOB_DEFINITIONS } from '../CronJobs.constants'
|
||||
import { buildCronQuery, buildHttpRequestCommand, parseCronJobCommand } from '../CronJobs.utils'
|
||||
@@ -53,9 +52,9 @@ import { CronJobScheduleSection } from './CronJobScheduleSection'
|
||||
interface CreateCronJobSheetProps {
|
||||
selectedCronJob?: Pick<CronJob, 'jobname' | 'schedule' | 'active' | 'command'>
|
||||
supportsSeconds: boolean
|
||||
isClosing: boolean
|
||||
setIsClosing: (v: boolean) => void
|
||||
onDirty: (isDirty: boolean) => void
|
||||
onClose: () => void
|
||||
onCloseWithConfirmation: () => void
|
||||
}
|
||||
|
||||
const FORM_ID = 'create-cron-job-sidepanel'
|
||||
@@ -87,9 +86,9 @@ const buildCommand = (values: CronJobType) => {
|
||||
export const CreateCronJobSheet = ({
|
||||
selectedCronJob,
|
||||
supportsSeconds,
|
||||
isClosing,
|
||||
setIsClosing,
|
||||
onDirty,
|
||||
onClose,
|
||||
onCloseWithConfirmation: confirmOnClose,
|
||||
}: CreateCronJobSheetProps) => {
|
||||
const { childId } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -129,17 +128,14 @@ export const CreateCronJobSheet = ({
|
||||
},
|
||||
})
|
||||
|
||||
const isEdited = form.formState.isDirty
|
||||
// if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet
|
||||
if (!isEdited && isClosing) onClose()
|
||||
useEffect(() => {
|
||||
const subscription = form.watch(() => {
|
||||
const isDirty = form.formState.isDirty
|
||||
onDirty(isDirty)
|
||||
})
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (isEdited) {
|
||||
setIsClosing(true)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
return () => subscription.unsubscribe()
|
||||
}, [form, onDirty])
|
||||
|
||||
const [
|
||||
cronType,
|
||||
@@ -430,7 +426,7 @@ export const CreateCronJobSheet = ({
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
onClick={confirmOnClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
@@ -447,18 +443,6 @@ export const CreateCronJobSheet = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isClosing}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosing(false)}
|
||||
onConfirm={() => onClose()}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
{pgNetExtension && (
|
||||
<EnableExtensionModal
|
||||
visible={showEnableExtensionModal}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NavigationItem, PageLayout } from 'components/layouts/PageLayout/PageLa
|
||||
import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query'
|
||||
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet'
|
||||
import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils'
|
||||
@@ -31,7 +33,6 @@ export const CronJobPage = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const [isEditSheetOpen, setIsEditSheetOpen] = useState(false)
|
||||
const [isClosing, setIsClosing] = useState(false)
|
||||
|
||||
const jobId = Number(childId)
|
||||
|
||||
@@ -50,6 +51,15 @@ export const CronJobPage = () => {
|
||||
const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop()
|
||||
const isValidEdgeFunction = edgeFunctions.some((x) => x.slug === edgeFunctionSlug)
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setIsDirty(false)
|
||||
setIsEditSheetOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
label: 'Integrations',
|
||||
@@ -177,16 +187,29 @@ export const CronJobPage = () => {
|
||||
command: job.command,
|
||||
}}
|
||||
supportsSeconds={true}
|
||||
isClosing={isClosing}
|
||||
setIsClosing={setIsClosing}
|
||||
onClose={() => {
|
||||
setIsEditSheetOpen(false)
|
||||
setIsClosing(false)
|
||||
}}
|
||||
onDirty={setIsDirty}
|
||||
onClose={() => setIsEditSheetOpen(false)}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -17,10 +17,12 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
|
||||
import { Button, cn, LoadingLine, Sheet, SheetContent } from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { formatCronJobColumns } from './CronJobs.utils'
|
||||
import { DeleteCronJob } from './DeleteCronJob'
|
||||
|
||||
@@ -42,8 +44,6 @@ export const CronjobsTab = () => {
|
||||
parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
|
||||
)
|
||||
|
||||
// used for confirmation prompt in the Create Cron Job Sheet
|
||||
const [isClosingCreateCronJobSheet, setIsClosingCreateCronJobSheet] = useState(false)
|
||||
const [cronJobForEditing, setCronJobForEditing] = useState<
|
||||
Pick<CronJob, 'jobname' | 'schedule' | 'active' | 'command'> | undefined
|
||||
>()
|
||||
@@ -131,6 +131,20 @@ export const CronjobsTab = () => {
|
||||
setCreateCronJobSheetShown(true)
|
||||
}
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const onClose = () => {
|
||||
setCronJobForEditing(undefined)
|
||||
setCreateCronJobSheetShown(false)
|
||||
cleanPointerEventsNoneOnBody(500)
|
||||
}
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setIsDirty(false)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full space-y-4">
|
||||
@@ -267,25 +281,33 @@ export const CronjobsTab = () => {
|
||||
cronJob={cronJobForDeletion!}
|
||||
/>
|
||||
|
||||
<Sheet
|
||||
open={!!createCronJobSheetShown}
|
||||
onOpenChange={() => setIsClosingCreateCronJobSheet(true)}
|
||||
>
|
||||
<Sheet open={!!createCronJobSheetShown} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="default" tabIndex={undefined}>
|
||||
<CreateCronJobSheet
|
||||
selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
|
||||
supportsSeconds={supportsSeconds}
|
||||
onClose={() => {
|
||||
setIsClosingCreateCronJobSheet(false)
|
||||
setCronJobForEditing(undefined)
|
||||
setCreateCronJobSheetShown(false)
|
||||
cleanPointerEventsNoneOnBody(500)
|
||||
}}
|
||||
isClosing={isClosingCreateCronJobSheet}
|
||||
setIsClosing={setIsClosingCreateCronJobSheet}
|
||||
onDirty={setIsDirty}
|
||||
onClose={onClose}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,9 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
|
||||
import { useDatabaseQueueCreateMutation } from 'data/database-queues/database-queues-create-mutation'
|
||||
import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -21,6 +23,8 @@ import {
|
||||
RadioGroupStacked,
|
||||
RadioGroupStackedItem,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
@@ -33,8 +37,7 @@ import { QUEUE_TYPES } from './Queues.constants'
|
||||
import { QueryNameSchema } from './Queues.utils'
|
||||
|
||||
export interface CreateQueueSheetProps {
|
||||
isClosing: boolean
|
||||
setIsClosing: (v: boolean) => void
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -67,13 +70,7 @@ export type QueueType = CreateQueueForm['values']
|
||||
|
||||
const FORM_ID = 'create-queue-sidepanel'
|
||||
|
||||
export const CreateQueueSheet = ({ isClosing, setIsClosing, onClose }: CreateQueueSheetProps) => {
|
||||
// This is for enabling pg_partman extension which will be used for partitioned queues (3rd kind of queue)
|
||||
// const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false)
|
||||
// const { can: canToggleExtensions } = useAsyncCheckPermissions(
|
||||
// PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
// 'extensions'
|
||||
// )
|
||||
export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) => {
|
||||
const router = useRouter()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
@@ -93,18 +90,22 @@ export const CreateQueueSheet = ({ isClosing, setIsClosing, onClose }: CreateQue
|
||||
},
|
||||
})
|
||||
|
||||
const isEdited = form.formState.isDirty
|
||||
|
||||
// if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet
|
||||
if (!isEdited && isClosing) onClose()
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (isEdited) {
|
||||
setIsClosing(true)
|
||||
} else {
|
||||
onClose()
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.reset()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
// end of stable references
|
||||
visible,
|
||||
])
|
||||
|
||||
const checkIsDirty = () => form.formState.isDirty
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<CreateQueueForm> = async ({ name, enableRls, values }) => {
|
||||
createQueue(
|
||||
@@ -143,250 +144,208 @@ export const CreateQueueSheet = ({ isClosing, setIsClosing, onClose }: CreateQue
|
||||
const queueType = form.watch('values.type')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create a new queue</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="default" className="w-[35%]" tabIndex={undefined}>
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create a new queue</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="overflow-auto flex-grow">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
className="flex-grow overflow-auto"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Name" layout="vertical" className="gap-1 relative">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
|
||||
Must be all lowercase letters
|
||||
</span>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.type"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Type" layout="vertical" className="gap-1">
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked
|
||||
id="queue_type"
|
||||
name="queue_type"
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{QUEUE_TYPES.map((definition) => (
|
||||
<RadioGroupStackedItem
|
||||
key={definition.value}
|
||||
id={definition.value}
|
||||
value={definition.value}
|
||||
label=""
|
||||
disabled={
|
||||
!pgPartmanExtensionInstalled && definition.value === 'partitioned'
|
||||
}
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex items-start gap-x-5">
|
||||
<div className="text-foreground">{definition.icon}</div>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-foreground text-left">{definition.label}</p>
|
||||
{definition.value === 'partitioned' && (
|
||||
<Badge variant="warning">Coming soon</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-foreground-lighter text-left">
|
||||
{definition.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* {!pgPartmanExtensionInstalled &&
|
||||
definition.value === 'partitioned' ? (
|
||||
<div className="w-full flex gap-x-2 pl-11 py-2 items-center">
|
||||
<WarningIcon />
|
||||
<span className="text-xs">
|
||||
<code>pg_partman</code> needs to be installed to use this type
|
||||
</span>
|
||||
</div>
|
||||
) : null} */}
|
||||
</RadioGroupStackedItem>
|
||||
))}
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{/* {!pgPartmanExtensionInstalled && (
|
||||
<Admonition
|
||||
type="note"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<span>
|
||||
Enable <code className="text-xs w-min">pg_partman</code> for partitioned
|
||||
queues
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<span>
|
||||
This will allow you to create partitioned queues which can handle a large
|
||||
amount of messages
|
||||
<div className="overflow-auto flex-grow">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
className="flex-grow overflow-auto"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Name" layout="vertical" className="gap-1 relative">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
|
||||
Must be all lowercase letters
|
||||
</span>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
className="w-min"
|
||||
disabled={!canToggleExtensions}
|
||||
onClick={() => setShowEnableExtensionModal(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: 'You need additional permissions to enable database extensions',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Install pg_partman extension
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
}
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
{queueType === 'partitioned' && (
|
||||
<>
|
||||
<SheetSection className="flex flex-col gap-3">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.partitionInterval"
|
||||
render={({ field: { ref, ...rest } }) => (
|
||||
<FormItemLayout label="Partition interval" className="gap-1">
|
||||
<Input
|
||||
{...rest}
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
actions={<p className="text-foreground-light pr-2">ms</p>}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.retentionInterval"
|
||||
render={({ field: { ref, ...rest } }) => (
|
||||
<FormItemLayout label="Retention interval" className="gap-1">
|
||||
<Input
|
||||
{...rest}
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
actions={<p className="text-foreground-light pr-2">ms</p>}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<SheetSection className="flex flex-col gap-y-2">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="enableRls"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex"
|
||||
label={
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p>Enable Row Level Security (RLS)</p>
|
||||
<Badge color="scale">Recommended</Badge>
|
||||
</div>
|
||||
}
|
||||
description="Restrict access to your queue by enabling RLS and writing Postgres policies to control access for each role."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Checkbox_Shadcn_
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={field.disabled || isExposed}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{!isExposed ? (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="Row Level Security for queues is only relevant if exposure through PostgREST has been enabled"
|
||||
>
|
||||
<Markdown
|
||||
className="[&>p]:!leading-normal"
|
||||
content={`You may opt to manage your queues via any Supabase client libraries or PostgREST
|
||||
endpoints by enabling this in the [queues settings](/project/${project?.ref}/integrations/queues/settings).`}
|
||||
/>
|
||||
</Admonition>
|
||||
) : (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="RLS must be enabled as queues are exposed via PostgREST"
|
||||
description="This is to prevent anonymous access to any of your queues"
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.type"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Type" layout="vertical" className="gap-1">
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked
|
||||
id="queue_type"
|
||||
name="queue_type"
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{QUEUE_TYPES.map((definition) => (
|
||||
<RadioGroupStackedItem
|
||||
key={definition.value}
|
||||
id={definition.value}
|
||||
value={definition.value}
|
||||
label=""
|
||||
disabled={
|
||||
!pgPartmanExtensionInstalled && definition.value === 'partitioned'
|
||||
}
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex items-start gap-x-5">
|
||||
<div className="text-foreground">{definition.icon}</div>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-foreground text-left">
|
||||
{definition.label}
|
||||
</p>
|
||||
{definition.value === 'partitioned' && (
|
||||
<Badge variant="warning">Coming soon</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-foreground-lighter text-left">
|
||||
{definition.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
))}
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
{queueType === 'partitioned' && (
|
||||
<>
|
||||
<SheetSection className="flex flex-col gap-3">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.partitionInterval"
|
||||
render={({ field: { ref, ...rest } }) => (
|
||||
<FormItemLayout label="Partition interval" className="gap-1">
|
||||
<Input
|
||||
{...rest}
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
actions={<p className="text-foreground-light pr-2">ms</p>}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.retentionInterval"
|
||||
render={({ field: { ref, ...rest } }) => (
|
||||
<FormItemLayout label="Retention interval" className="gap-1">
|
||||
<Input
|
||||
{...rest}
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
actions={<p className="text-foreground-light pr-2">ms</p>}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
</SheetSection>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<SheetSection className="flex flex-col gap-y-2">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="enableRls"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex"
|
||||
label={
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p>Enable Row Level Security (RLS)</p>
|
||||
<Badge color="scale">Recommended</Badge>
|
||||
</div>
|
||||
}
|
||||
description="Restrict access to your queue by enabling RLS and writing Postgres policies to control access for each role."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Checkbox_Shadcn_
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={field.disabled || isExposed}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{!isExposed ? (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="Row Level Security for queues is only relevant if exposure through PostgREST has been enabled"
|
||||
>
|
||||
<Markdown
|
||||
className="[&>p]:!leading-normal"
|
||||
content={`You may opt to manage your queues via any Supabase client libraries or PostgREST
|
||||
endpoints by enabling this in the [queues settings](/project/${project?.ref}/integrations/queues/settings).`}
|
||||
/>
|
||||
</Admonition>
|
||||
) : (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="RLS must be enabled as queues are exposed via PostgREST"
|
||||
description="This is to prevent anonymous access to any of your queues"
|
||||
/>
|
||||
)}
|
||||
</SheetSection>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={confirmOnClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
form={FORM_ID}
|
||||
htmlType="submit"
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create queue
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
form={FORM_ID}
|
||||
htmlType="submit"
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create queue
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isClosing}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosing(false)}
|
||||
onConfirm={() => onClose()}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
{/* {pgPartmanExtension && (
|
||||
<EnableExtensionModal
|
||||
visible={showEnableExtensionModal}
|
||||
extension={pgPartmanExtension}
|
||||
onCancel={() => setShowEnableExtensionModal(false)}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useQueuesQuery } from 'data/database-queues/database-queues-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Button, cn, LoadingLine, Sheet, SheetContent } from 'ui'
|
||||
import { Button, cn, LoadingLine } from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { CreateQueueSheet } from './CreateQueueSheet'
|
||||
import { formatQueueColumns, prepareQueuesForDataGrid } from './Queues.utils'
|
||||
@@ -22,8 +22,6 @@ export const QueuesTab = () => {
|
||||
const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault(''))
|
||||
const [search, setSearch] = useState(searchQuery)
|
||||
|
||||
// used for confirmation prompt in the Create Queue Sheet
|
||||
const [isClosingCreateQueueSheet, setIsClosingCreateQueueSheet] = useState(false)
|
||||
const [createQueueSheetShown, setCreateQueueSheetShown] = useState(false)
|
||||
|
||||
const {
|
||||
@@ -162,18 +160,12 @@ export const QueuesTab = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sheet open={createQueueSheetShown} onOpenChange={() => setIsClosingCreateQueueSheet(true)}>
|
||||
<SheetContent size="default" className="w-[35%]" tabIndex={undefined}>
|
||||
<CreateQueueSheet
|
||||
onClose={() => {
|
||||
setIsClosingCreateQueueSheet(false)
|
||||
setCreateQueueSheetShown(false)
|
||||
}}
|
||||
isClosing={isClosingCreateQueueSheet}
|
||||
setIsClosing={setIsClosingCreateQueueSheet}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CreateQueueSheet
|
||||
visible={createQueueSheetShown}
|
||||
onClose={() => {
|
||||
setCreateQueueSheetShown(false)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { CreateWrapperSheetProps } from './CreateWrapperSheet'
|
||||
import InputField from './InputField'
|
||||
import { makeValidateRequired } from './Wrappers.utils'
|
||||
@@ -58,9 +57,9 @@ type Target = 'S3Tables' | 'R2Catalog' | 'IcebergRestCatalog'
|
||||
|
||||
export const CreateIcebergWrapperSheet = ({
|
||||
wrapperMeta: wrapperMetaOriginal,
|
||||
isClosing,
|
||||
setIsClosing,
|
||||
onDirty,
|
||||
onClose,
|
||||
onCloseWithConfirmation,
|
||||
}: CreateWrapperSheetProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
@@ -187,21 +186,9 @@ export const CreateIcebergWrapperSheet = ({
|
||||
onSubmit={onSubmit}
|
||||
className="flex-grow flex flex-col h-full"
|
||||
>
|
||||
{({ values, initialValues, setFieldValue }: any) => {
|
||||
{({ values, initialValues }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (hasChanges) {
|
||||
setIsClosing(true)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
// if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet
|
||||
if (!hasChanges && isClosing) {
|
||||
onClose()
|
||||
}
|
||||
onDirty(hasChanges)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -349,7 +336,7 @@ export const CreateIcebergWrapperSheet = ({
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
onClick={onCloseWithConfirmation}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
@@ -369,18 +356,6 @@ export const CreateIcebergWrapperSheet = ({
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isClosing}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosing(false)}
|
||||
onConfirm={() => onClose()}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
SheetTitle,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import InputField from './InputField'
|
||||
import { WrapperMeta } from './Wrappers.types'
|
||||
import { makeValidateRequired } from './Wrappers.utils'
|
||||
@@ -33,17 +32,17 @@ import WrapperTableEditor from './WrapperTableEditor'
|
||||
const FORM_ID = 'create-wrapper-form'
|
||||
|
||||
export interface CreateWrapperSheetProps {
|
||||
isClosing: boolean
|
||||
wrapperMeta: WrapperMeta
|
||||
setIsClosing: (v: boolean) => void
|
||||
onDirty: (isDirty: boolean) => void
|
||||
onClose: () => void
|
||||
onCloseWithConfirmation: () => void
|
||||
}
|
||||
|
||||
export const CreateWrapperSheet = ({
|
||||
wrapperMeta,
|
||||
isClosing,
|
||||
setIsClosing,
|
||||
onDirty,
|
||||
onClose,
|
||||
onCloseWithConfirmation,
|
||||
}: CreateWrapperSheetProps) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -53,7 +52,6 @@ export const CreateWrapperSheet = ({
|
||||
|
||||
const [newTables, setNewTables] = useState<any[]>([])
|
||||
const [isEditingTable, setIsEditingTable] = useState(false)
|
||||
const [createSchemaSheetOpen, setCreateSchemaSheetOpen] = useState(false)
|
||||
const [selectedTableToEdit, setSelectedTableToEdit] = useState()
|
||||
const [selectedMode, setSelectedMode] = useState<'tables' | 'schema'>(
|
||||
wrapperMeta.tables.length > 0 ? 'tables' : 'schema'
|
||||
@@ -197,21 +195,9 @@ export const CreateWrapperSheet = ({
|
||||
onSubmit={onSubmit}
|
||||
className="flex-grow flex flex-col h-full"
|
||||
>
|
||||
{({ values, initialValues, setFieldValue }: any) => {
|
||||
{({ values, initialValues }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (hasChanges) {
|
||||
setIsClosing(true)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
// if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet
|
||||
if (!hasChanges && isClosing) {
|
||||
onClose()
|
||||
}
|
||||
onDirty(hasChanges)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -453,7 +439,7 @@ export const CreateWrapperSheet = ({
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
onClick={onCloseWithConfirmation}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
@@ -473,18 +459,6 @@ export const CreateWrapperSheet = ({
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isClosing}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosing(false)}
|
||||
onConfirm={() => onClose()}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { Edit, Trash } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
|
||||
@@ -11,6 +11,7 @@ import { FDW } from 'data/fdw/fdws-query'
|
||||
import { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query'
|
||||
import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import InputField from './InputField'
|
||||
@@ -66,6 +67,7 @@ export const EditWrapperSheet = ({
|
||||
undefined
|
||||
)
|
||||
const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
|
||||
const hasChangesRef = useRef(false)
|
||||
|
||||
const initialValues = {
|
||||
wrapper_name: wrapper?.name,
|
||||
@@ -109,6 +111,23 @@ export const EditWrapperSheet = ({
|
||||
})
|
||||
}
|
||||
|
||||
const checkIsDirty = useCallback(() => hasChangesRef.current, [])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClosing) return
|
||||
if (checkIsDirty()) {
|
||||
confirmOnClose()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
setIsClosing(false)
|
||||
}, [checkIsDirty, confirmOnClose, isClosing, onClose, setIsClosing])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
@@ -131,22 +150,10 @@ export const EditWrapperSheet = ({
|
||||
const hasFormChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
const hasTableChanges = JSON.stringify(initialTables) !== JSON.stringify(wrapperTables)
|
||||
const hasChanges = hasFormChanges || hasTableChanges
|
||||
hasChangesRef.current = hasChanges
|
||||
|
||||
const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (hasChanges) {
|
||||
setIsClosing(true)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
// if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet
|
||||
if (!hasChanges && isClosing) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
// [Alaister] although this "technically" is breaking the rules of React hooks
|
||||
// it won't error because the hooks are always rendered in the same order
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
@@ -321,7 +328,7 @@ export const EditWrapperSheet = ({
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClosePanel}
|
||||
onClick={confirmOnClose}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
@@ -343,18 +350,7 @@ export const EditWrapperSheet = ({
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isClosing}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosing(false)}
|
||||
onConfirm={() => onClose()}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
@@ -369,3 +365,18 @@ export const EditWrapperSheet = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
SheetContent,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
|
||||
import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
|
||||
import { CreateWrapperSheet } from './CreateWrapperSheet'
|
||||
@@ -27,7 +29,6 @@ export const WrapperOverviewTab = () => {
|
||||
const { id } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [createWrapperShown, setCreateWrapperShown] = useState(false)
|
||||
const [isClosingCreateWrapper, setisClosingCreateWrapper] = useState(false)
|
||||
|
||||
const { can: canCreateWrapper } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
@@ -39,6 +40,15 @@ export const WrapperOverviewTab = () => {
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setCreateWrapperShown(false)
|
||||
setIsDirty(false)
|
||||
},
|
||||
})
|
||||
|
||||
const wrapperMeta = WRAPPERS.find((w) => w.name === id)
|
||||
|
||||
if (!wrapperMeta) {
|
||||
@@ -127,19 +137,34 @@ export const WrapperOverviewTab = () => {
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={() => setisClosingCreateWrapper(true)}>
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="lg" tabIndex={undefined}>
|
||||
<CreateWrapperSheetComponent
|
||||
wrapperMeta={wrapperMeta}
|
||||
onDirty={(dirty) => setIsDirty(dirty)}
|
||||
onClose={() => {
|
||||
setCreateWrapperShown(false)
|
||||
setisClosingCreateWrapper(false)
|
||||
}}
|
||||
isClosing={isClosingCreateWrapper}
|
||||
setIsClosing={setisClosingCreateWrapper}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</IntegrationOverviewTab>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,9 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { FDW, useFDWsQuery } from 'data/fdw/fdws-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Sheet, SheetContent } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { CreateWrapperSheet } from './CreateWrapperSheet'
|
||||
import DeleteWrapperModal from './DeleteWrapperModal'
|
||||
import { WRAPPERS } from './Wrappers.constants'
|
||||
@@ -18,7 +20,6 @@ export const WrappersTab = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [selectedWrapperForDelete, setSelectedWrapperForDelete] = useState<FDW | null>(null)
|
||||
const [createWrapperShown, setCreateWrapperShown] = useState(false)
|
||||
const [isClosingCreateWrapper, setisClosingCreateWrapper] = useState(false)
|
||||
|
||||
const { can: canCreateWrapper } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
@@ -38,28 +39,34 @@ export const WrappersTab = () => {
|
||||
? wrappers.filter((w) => wrapperMetaComparator(wrapperMeta, w))
|
||||
: []
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: useCallback(() => isDirty, [isDirty]),
|
||||
onClose: useCallback(() => {
|
||||
setCreateWrapperShown(false)
|
||||
setIsDirty(false)
|
||||
}, []),
|
||||
})
|
||||
|
||||
const Container = useCallback(
|
||||
({ ...props }: { children: ReactNode } & HTMLProps<HTMLDivElement>) => (
|
||||
<div className="w-full mx-10 py-10 ">
|
||||
{props.children}
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={() => setisClosingCreateWrapper(true)}>
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="lg" tabIndex={undefined}>
|
||||
{wrapperMeta && (
|
||||
<CreateWrapperSheet
|
||||
wrapperMeta={wrapperMeta}
|
||||
onClose={() => {
|
||||
setCreateWrapperShown(false)
|
||||
setisClosingCreateWrapper(false)
|
||||
}}
|
||||
isClosing={isClosingCreateWrapper}
|
||||
setIsClosing={setisClosingCreateWrapper}
|
||||
onDirty={setIsDirty}
|
||||
onClose={() => setCreateWrapperShown(false)}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
),
|
||||
[createWrapperShown, wrapperMeta, isClosingCreateWrapper]
|
||||
[createWrapperShown, wrapperMeta, confirmOnClose]
|
||||
)
|
||||
|
||||
if (!wrapperMeta) {
|
||||
@@ -103,6 +110,22 @@ export const WrappersTab = () => {
|
||||
onClose={() => setSelectedWrapperForDelete(null)}
|
||||
/>
|
||||
)}
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import { RetrieveTableResult } from 'data/tables/table-retrieve-query'
|
||||
import { getTables } from 'data/tables/tables-query'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
@@ -133,7 +134,14 @@ export const SidePanelEditor = ({
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
|
||||
const [isEdited, setIsEdited] = useState<boolean>(false)
|
||||
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isEdited,
|
||||
onClose: () => {
|
||||
setIsEdited(false)
|
||||
snap.closeSidePanel()
|
||||
},
|
||||
})
|
||||
|
||||
const enumArrayColumns = (selectedTable?.columns ?? [])
|
||||
.filter((column) => {
|
||||
@@ -632,13 +640,7 @@ export const SidePanelEditor = ({
|
||||
snap.closeSidePanel()
|
||||
}
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (isEdited) {
|
||||
setIsClosingPanel(true)
|
||||
} else {
|
||||
snap.closeSidePanel()
|
||||
}
|
||||
}
|
||||
const onClosePanel = confirmOnClose
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -730,22 +732,22 @@ export const SidePanelEditor = ({
|
||||
closePanel={onClosePanel}
|
||||
updateEditorDirty={setIsEdited}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
setIsEdited(false)
|
||||
snap.closeSidePanel()
|
||||
}}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
|
||||
<ConfirmationModal
|
||||
visible={visible}
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
|
||||
56
apps/studio/hooks/ui/useConfirmOnClose.tsx
Normal file
56
apps/studio/hooks/ui/useConfirmOnClose.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import useLatest from '../misc/useLatest'
|
||||
|
||||
export interface ConfirmOnCloseModalProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
interface UseConfirmOnCloseProps {
|
||||
checkIsDirty: () => boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const useConfirmOnClose = ({ checkIsDirty, onClose }: UseConfirmOnCloseProps) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const checkIsDirtyRef = useLatest(checkIsDirty)
|
||||
const onCloseRef = useLatest(onClose)
|
||||
|
||||
const confirmOnClose = useCallback(() => {
|
||||
if (checkIsDirtyRef.current()) {
|
||||
setVisible(true)
|
||||
} else {
|
||||
onCloseRef.current()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
setVisible(false)
|
||||
onCloseRef.current()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
const modalProps: ConfirmOnCloseModalProps = useMemo(
|
||||
() => ({
|
||||
visible,
|
||||
onClose: onConfirm,
|
||||
onCancel,
|
||||
}),
|
||||
[visible, onConfirm, onCancel]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
confirmOnClose,
|
||||
modalProps,
|
||||
}),
|
||||
[confirmOnClose, modalProps]
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user