mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): replace CloseConfirmationModal with DiscardChangesConfirmationDialog (#43430)
## What kind of change does this PR introduce? Form handling improvement. ## What is the current behavior? https://github.com/supabase/supabase/pull/43201/ standardised our discard changes behaviour with a shared hook and `DiscardChangesConfirmationDialog` component. But many forms and sheets still: 1. Don’t have any Discard-confirm close behaviour, making it too easy to make accidental discards 2. Use a more complicated, manually-created `CloseConfirmationModal` approach ## What is the new behavior? - Replaced all instances of `#2` above that had `CloseConfirmationModal` with `DiscardChangesConfirmationDialog` and its hook - Improved design system documentation around dirty form dismissal | Before | After | | --- | --- | | <img width="987" height="569" alt="Mercor Apexroles Foo Supabase-9A40EC7C-F335-4B26-B567-450FC0845463" src="https://github.com/user-attachments/assets/363bed82-34d2-4cc8-9164-6d18cfdbdbbc" /> | <img width="987" height="569" alt="Mercor Apexroles Foo Supabase-F427F1FA-DECC-4194-B663-A9E5A6F285A1" src="https://github.com/user-attachments/assets/d49fafdc-a5c2-46df-9b67-ec42bacbe716" /> | ## To test Try editing values these sheets in staging, then blurring the sheet or pressing `esc`: - CreateQueueSheet.tsx - CronJobsTab.tsx - CronJobPage.tsx - EditWrapperSheet.tsx - OverviewTab.tsx - WrappersTab.tsx - CreateFunction/index.tsx - EditHookPanel.tsx - TriggerSheet.tsx - SidePanelEditor.tsx - EditSecretSheet.tsx - PolicyEditorModal/index.tsx - PolicyEditorPanel/index.tsx ## Still to come - [ ] Incrementally take on `#1`: implement `DiscardChangesConfirmationDialog` and its hook in sheets or dialog forms that have no dirty form dismissal handling --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
a832d62785
commit
8a5ad58f81
25 files changed
+718
-721
No files matched your search
@@ -1292,6 +1292,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"sheet-confirm-on-close-demo": {
|
||||
name: "sheet-confirm-on-close-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["alert-dialog","button","input","label","separator","sheet"],
|
||||
component: React.lazy(() => import("@/registry/default/example/sheet-confirm-on-close-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/sheet-confirm-on-close-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"sheet-demo": {
|
||||
name: "sheet-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -10,7 +10,7 @@ Use Confirmation Modal when the user needs extra context to make a decision, suc
|
||||
|
||||
If the confirmation can be expressed as a single short paragraph, use [Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See [Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern.
|
||||
|
||||
For dirty-form dismissal in dialogs/sheets, prefer the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) rather than creating new local `CloseConfirmationModal` wrappers. The ad-hoc `CloseConfirmationModal` wrapper pattern is deprecated for new implementations.
|
||||
For dirty-form dismissal in dialogs/sheets, use the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) instead of `ConfirmationModal`. Avoid creating custom wrapper components for this flow; wire `modalProps` from `useConfirmOnClose` directly into `DiscardChangesConfirmationDialog`.
|
||||
|
||||
<ComponentPreview name="confirmation-modal-demo" peekCode wide />
|
||||
|
||||
|
||||
@@ -18,18 +18,6 @@ We have two main ways of handling modality:
|
||||
|
||||
As a general rule: use dialogs for short, focused tasks and use sheets for longer forms or more detailed views.
|
||||
|
||||
### Dirty form dismissal pattern
|
||||
|
||||
When a dialog or sheet contains a form, users should generally be allowed to attempt dismissal via all normal affordances (backdrop click, Escape key, close icon, and footer `Cancel` button).
|
||||
|
||||
If the form is clean, close immediately. If the form has unsaved changes, show a discard-confirmation dialog instead of closing immediately.
|
||||
|
||||
This pattern is implemented in Studio with `useConfirmOnClose` plus `DiscardChangesConfirmationDialog`.
|
||||
|
||||
- **Prompt on footer `Cancel` too:** If a button is labeled `Cancel`, users expect it to stop the current action, not silently discard edits. Prompting keeps behavior consistent with backdrop/Escape dismissal and prevents accidental loss.
|
||||
- **Use explicit labels for no-prompt discard:** If you intentionally want a one-click destructive exit, label the action `Discard` (or `Discard changes`) rather than `Cancel`.
|
||||
- **Guard close attempts, not unmounts:** This pattern should intercept controlled modal/sheet close attempts (`onOpenChange`, close buttons, footer actions). It should not attempt to block route changes or arbitrary component unmounts.
|
||||
|
||||
## Dialogs
|
||||
|
||||
Dialogs are centered overlays used for short, focused tasks. All dialogs should follow these best practices:
|
||||
@@ -53,22 +41,6 @@ There are quite a few dialog components, each suited to a different task or cont
|
||||
|
||||
<ComponentPreview name="alert-dialog-demo" />
|
||||
|
||||
#### Discard changes confirmation dialog (pattern)
|
||||
|
||||
For dirty form dismissal, use a short discard-confirmation dialog after a close attempt instead of disabling dismissal entirely.
|
||||
|
||||
- The primary form remains in a `Dialog` or `Sheet` (dismissible).
|
||||
- Closing is intercepted only when the form is dirty.
|
||||
- The follow-up confirmation is an `AlertDialog` pattern (`DiscardChangesConfirmationDialog` in Studio).
|
||||
|
||||
Typical flow:
|
||||
|
||||
1. User attempts to close the dialog/sheet (backdrop, Escape, close icon, or `Cancel`)
|
||||
2. If the form is clean, close immediately
|
||||
3. If the form is dirty, show discard confirmation
|
||||
4. `Keep editing` returns to the form
|
||||
5. `Discard changes` closes and resets the form
|
||||
|
||||
#### Text Confirm Dialog
|
||||
|
||||
[Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. The confirm action remains disabled until the input matches.
|
||||
@@ -102,3 +74,53 @@ Sheets are dialogs presented as side panels. Use them for content that is larger
|
||||
[Sheet](../components/sheet) is modal by default, blocking interaction with the underlying page.
|
||||
|
||||
<ComponentPreview name="sheet-demo" />
|
||||
|
||||
## Best practices
|
||||
|
||||
### Dirty form dismissal
|
||||
|
||||
When a dialog or sheet contains a form, keep all normal dismissal affordances enabled (backdrop click, Escape key, close icon, and footer `Cancel` button).
|
||||
|
||||
Decision flow:
|
||||
|
||||
1. User attempts to close the dialog/sheet.
|
||||
2. If the form is clean, close immediately.
|
||||
3. If the form is dirty, show a discard-confirmation dialog.
|
||||
4. `Keep editing` returns to the form.
|
||||
5. `Discard changes` closes and resets the form.
|
||||
|
||||
Implementation checklist:
|
||||
|
||||
- Intercept close attempts from `onOpenChange`.
|
||||
- Route footer `Cancel` through the same close guard.
|
||||
- Render a separate discard confirmation dialog when dirty.
|
||||
- Keep `Cancel` non-destructive; use `Discard`/`Discard changes` for one-click destructive exits.
|
||||
- Guard controlled close attempts only; do not try to block route changes or arbitrary unmounts.
|
||||
|
||||
Studio implementation (preferred in Studio code):
|
||||
|
||||
```tsx
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
...
|
||||
<Button type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
...
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</Sheet>
|
||||
```
|
||||
|
||||
Generic implementation (outside Studio):
|
||||
|
||||
- If Studio-only helpers are unavailable, recreate the same behavior with `AlertDialog`.
|
||||
- The demo below shows the same flow and API shape (`confirmOnClose`, `handleOpenChange`, `modalProps`).
|
||||
|
||||
<ComponentPreview name="sheet-confirm-on-close-demo" />
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
Input_Shadcn_ as Input,
|
||||
Label_Shadcn_ as Label,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
|
||||
interface EndpointValues {
|
||||
endpointUrl: string
|
||||
secretHeader: string
|
||||
}
|
||||
|
||||
interface ConfirmOnCloseModalProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const defaultValues: EndpointValues = {
|
||||
endpointUrl: '',
|
||||
secretHeader: '',
|
||||
}
|
||||
|
||||
const useConfirmOnClose = ({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
}: {
|
||||
checkIsDirty: () => boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const confirmOnClose = useCallback(() => {
|
||||
if (checkIsDirty()) {
|
||||
setVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
onClose()
|
||||
}, [checkIsDirty, onClose])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
confirmOnClose()
|
||||
}
|
||||
},
|
||||
[confirmOnClose]
|
||||
)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
setVisible(false)
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
const modalProps: ConfirmOnCloseModalProps = useMemo(
|
||||
() => ({
|
||||
visible,
|
||||
onClose: onConfirm,
|
||||
onCancel,
|
||||
}),
|
||||
[visible, onConfirm, onCancel]
|
||||
)
|
||||
|
||||
return {
|
||||
confirmOnClose,
|
||||
handleOpenChange,
|
||||
modalProps,
|
||||
}
|
||||
}
|
||||
|
||||
const DiscardChangesAlertDialog = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => {
|
||||
const isConfirmingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
isConfirmingRef.current = false
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
isConfirmingRef.current = true
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) return
|
||||
|
||||
if (isConfirmingRef.current) {
|
||||
isConfirmingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
onCancel()
|
||||
},
|
||||
[onCancel]
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Any unsaved changes to this endpoint will be lost.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep editing</AlertDialogCancel>
|
||||
<AlertDialogAction variant="danger" onClick={handleConfirm}>
|
||||
Discard changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SheetConfirmOnCloseDemo() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [savedValues, setSavedValues] = useState<EndpointValues>(defaultValues)
|
||||
const [draftValues, setDraftValues] = useState<EndpointValues>(defaultValues)
|
||||
|
||||
const isDirty = useMemo(
|
||||
() =>
|
||||
draftValues.endpointUrl !== savedValues.endpointUrl ||
|
||||
draftValues.secretHeader !== savedValues.secretHeader,
|
||||
[draftValues, savedValues]
|
||||
)
|
||||
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setDraftValues(savedValues)
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const openSheet = () => {
|
||||
setDraftValues(savedValues)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
setSavedValues(draftValues)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="default" onClick={openSheet}>
|
||||
Open endpoint sheet
|
||||
</Button>
|
||||
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit endpoint</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Separator />
|
||||
<SheetSection className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endpoint-url">Endpoint URL</Label>
|
||||
<Input
|
||||
id="endpoint-url"
|
||||
value={draftValues.endpointUrl}
|
||||
placeholder="https://api.example.com/webhooks/supabase"
|
||||
onChange={(event) =>
|
||||
setDraftValues((current) => ({
|
||||
...current,
|
||||
endpointUrl: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secret-header">Secret header</Label>
|
||||
<Input
|
||||
id="secret-header"
|
||||
value={draftValues.secretHeader}
|
||||
placeholder="Bearer top-secret-value"
|
||||
onChange={(event) =>
|
||||
setDraftValues((current) => ({
|
||||
...current,
|
||||
secretHeader: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<SheetFooter>
|
||||
<Button type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={saveChanges} disabled={!isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<DiscardChangesAlertDialog {...modalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -761,6 +761,12 @@ export const examples: Registry = [
|
||||
registryDependencies: ['separator'],
|
||||
files: ['example/separator-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'sheet-confirm-on-close-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['alert-dialog', 'button', 'input', 'label', 'separator', 'sheet'],
|
||||
files: ['example/sheet-confirm-on-close-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'sheet-demo',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { has, isEmpty, isEqual } from 'lodash'
|
||||
|
||||
import { ident } from '@supabase/pg-meta/src/pg-format'
|
||||
import type { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { generateSqlPolicy } from 'data/ai/sql-policy-mutation'
|
||||
import type { CreatePolicyBody } from 'data/database-policies/database-policy-create-mutation'
|
||||
import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
|
||||
import { has, isEmpty, isEqual } from 'lodash'
|
||||
|
||||
import {
|
||||
PolicyFormField,
|
||||
PolicyForReview,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
|
||||
export const createSQLPolicy = (
|
||||
policyFormFields: PolicyFormField,
|
||||
originalPolicyFormFields: PostgresPolicy
|
||||
originalPolicyFormFields?: PostgresPolicy
|
||||
) => {
|
||||
const { definition, check } = policyFormFields
|
||||
const formattedPolicyFormFields = {
|
||||
@@ -32,7 +32,7 @@ export const createSQLPolicy = (
|
||||
check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check,
|
||||
}
|
||||
|
||||
if (isEmpty(originalPolicyFormFields)) {
|
||||
if (!originalPolicyFormFields || isEmpty(originalPolicyFormFields)) {
|
||||
return createSQLStatementForCreatePolicy(formattedPolicyFormFields)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface PolicyEditorProps {
|
||||
onReviewPolicy: () => void
|
||||
}
|
||||
|
||||
const PolicyEditor = ({
|
||||
export const PolicyEditor = ({
|
||||
isNewPolicy = true,
|
||||
policyFormFields = {},
|
||||
onUpdatePolicyFormFields = () => {},
|
||||
@@ -75,5 +75,3 @@ const PolicyEditor = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PolicyEditor
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { isEmpty, noop } from 'lodash'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Modal } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
|
||||
import {
|
||||
PolicyFormField,
|
||||
PolicyForReview,
|
||||
PostgresPolicyCreatePayload,
|
||||
PostgresPolicyUpdatePayload,
|
||||
} from '../Policies.types'
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
createPayloadForUpdatePolicy,
|
||||
createSQLPolicy,
|
||||
} from '../Policies.utils'
|
||||
import PolicyEditor from '../PolicyEditor'
|
||||
import { PolicyEditor } from '../PolicyEditor'
|
||||
import { PolicyReview } from '../PolicyReview'
|
||||
import PolicySelection from '../PolicySelection'
|
||||
import PolicyTemplates from '../PolicyTemplates'
|
||||
@@ -30,7 +32,7 @@ interface PolicyEditorModalProps {
|
||||
visible?: boolean
|
||||
schema?: string
|
||||
table?: string
|
||||
selectedPolicyToEdit: any
|
||||
selectedPolicyToEdit?: PostgresPolicy
|
||||
showAssistantPreview?: boolean
|
||||
onSelectCancel: () => void
|
||||
onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise<boolean>
|
||||
@@ -38,11 +40,11 @@ interface PolicyEditorModalProps {
|
||||
onSaveSuccess: () => void
|
||||
}
|
||||
|
||||
const PolicyEditorModal = ({
|
||||
export const PolicyEditorModal = ({
|
||||
visible = false,
|
||||
schema = '',
|
||||
table = '',
|
||||
selectedPolicyToEdit = {},
|
||||
selectedPolicyToEdit,
|
||||
showAssistantPreview = false,
|
||||
onSelectCancel = noop,
|
||||
onCreatePolicy,
|
||||
@@ -71,10 +73,10 @@ const PolicyEditorModal = ({
|
||||
const [policyFormFields, setPolicyFormFields] = useState<PolicyFormField>(
|
||||
initializedPolicyFormFields
|
||||
)
|
||||
const [policyStatementForReview, setPolicyStatementForReview] = useState<any>('')
|
||||
const [policyStatementForReview, setPolicyStatementForReview] = useState<PolicyForReview>()
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
onSelectCancel()
|
||||
@@ -198,7 +200,7 @@ const PolicyEditorModal = ({
|
||||
onCancel={confirmOnClose}
|
||||
>
|
||||
<div>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
{view === POLICY_MODAL_VIEWS.SELECTION ? (
|
||||
<PolicySelection
|
||||
description="Write rules with PostgreSQL's policies to fit your unique business needs."
|
||||
@@ -221,7 +223,7 @@ const PolicyEditorModal = ({
|
||||
templatesNote="* References a specific column in the table"
|
||||
onUseTemplate={onUseTemplate}
|
||||
/>
|
||||
) : view === POLICY_MODAL_VIEWS.REVIEW ? (
|
||||
) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? (
|
||||
<PolicyReview
|
||||
policy={policyStatementForReview}
|
||||
onSelectBack={onViewEditor}
|
||||
@@ -232,20 +234,3 @@ const PolicyEditorModal = ({
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -12,12 +12,13 @@ import * as z from 'zod'
|
||||
import { useParams } from 'common'
|
||||
import { IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation'
|
||||
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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
Tabs_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { LockedCreateQuerySection, LockedRenameQuerySection } from './LockedQuerySection'
|
||||
import { PolicyDetailsV2 } from './PolicyDetailsV2'
|
||||
import { checkIfPolicyHasChanged, generateCreatePolicyQuery } from './PolicyEditorPanel.utils'
|
||||
@@ -165,7 +165,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
return policyCreateUnsaved || policyUpdateUnsaved
|
||||
}, [command, name, roles, selectedPolicy])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: hasUnsavedChanges,
|
||||
onClose: onSelectCancel,
|
||||
})
|
||||
@@ -292,7 +292,7 @@ export const PolicyEditorPanel = memo(function ({
|
||||
<>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={showTools ? 'lg' : 'default'}
|
||||
@@ -573,24 +573,12 @@ export const PolicyEditorPanel = memo(function ({
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog
|
||||
{...modalProps}
|
||||
description="Are you sure you want to close the editor? Any unsaved changes on your policy and conversations with the Assistant will be lost."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
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>
|
||||
)
|
||||
@@ -64,5 +64,3 @@ export const PolicyReview = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PolicyReview
|
||||
@@ -13,7 +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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import type { FormSchema } from 'types'
|
||||
import {
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
Toggle,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { convertArgumentTypes, convertConfigParams } from '../Functions.utils'
|
||||
import { CreateFunctionHeader } from './CreateFunctionHeader'
|
||||
@@ -87,7 +87,7 @@ export const CreateFunction = ({
|
||||
})
|
||||
const language = form.watch('language')
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
@@ -158,7 +158,7 @@ export const CreateFunction = ({
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={'default'}
|
||||
@@ -402,27 +402,12 @@ export const CreateFunction = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</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
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import { Button, Form_Shadcn_, SidePanel } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
import { FormSchema, WebhookFormValues } from './EditHookPanel.constants'
|
||||
import { FormContents } from './FormContents'
|
||||
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation'
|
||||
import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-transaction-mutation'
|
||||
import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query'
|
||||
import { tableEditorQueryOptions } from '@/data/table-editor/table-editor-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from '@/hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
|
||||
import { uuidv4 } from '@/lib/helpers'
|
||||
|
||||
export type HTTPArgument = { id: string; name: string; value: string }
|
||||
@@ -288,7 +288,7 @@ export const EditHookPanel = () => {
|
||||
|
||||
// This is intentionally kept outside of the useConfirmOnClose hook to force RHF to update the isDirty state.
|
||||
const isDirty = form.formState.isDirty
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => onClose(),
|
||||
})
|
||||
@@ -340,22 +340,7 @@ export const EditHookPanel = () => {
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</SidePanel>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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,18 +1,17 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Terminal } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { PostgresTrigger } from '@supabase/postgres-meta'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import FormBoxEmpty from 'components/ui/FormBoxEmpty'
|
||||
import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation'
|
||||
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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import { Terminal } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -33,8 +32,9 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import * as z from 'zod'
|
||||
|
||||
import ChooseFunctionForm from './ChooseFunctionForm'
|
||||
import {
|
||||
TRIGGER_ENABLED_MODES,
|
||||
@@ -135,7 +135,7 @@ export const TriggerSheet = ({
|
||||
})
|
||||
const { function_name, function_schema } = form.watch()
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose,
|
||||
})
|
||||
@@ -189,7 +189,7 @@ export const TriggerSheet = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="lg" className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
@@ -481,7 +481,7 @@ export const TriggerSheet = ({
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -489,25 +489,10 @@ export const TriggerSheet = ({
|
||||
visible={showFunctionSelector}
|
||||
setVisible={setShowFunctionSelector}
|
||||
onChange={(fn) => {
|
||||
form.setValue('function_name', fn.name)
|
||||
form.setValue('function_schema', fn.schema)
|
||||
form.setValue('function_name', fn.name, { shouldDirty: true })
|
||||
form.setValue('function_schema', fn.schema, { shouldDirty: true })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
+82
-141
@@ -1,27 +1,22 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { SubmitHandler, useForm, type UseFormReturn } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation'
|
||||
import { ProjectSecret } from 'data/secrets/secrets-query'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Eye, EyeOff, X } from 'lucide-react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { useLatest } from 'react-use'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
Input,
|
||||
Input_Shadcn_,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
@@ -29,6 +24,7 @@ import {
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import z from 'zod'
|
||||
|
||||
const FORM_ID = 'edit-secret-sidepanel'
|
||||
|
||||
@@ -46,22 +42,17 @@ interface EditSecretSheetProps {
|
||||
}
|
||||
|
||||
export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetProps) {
|
||||
const { ref: projectRef } = useParams()
|
||||
const secretName = useLatest(secret?.name)
|
||||
const [showSecretValue, setShowSecretValue] = useState(false)
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.reset({
|
||||
name: secretName.current ?? '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
}, [form, secretName, visible])
|
||||
|
||||
const isValid = form.formState.isValid
|
||||
const isDirty = form.formState.isDirty
|
||||
|
||||
const { ref: projectRef } = useParams()
|
||||
const { mutate: updateSecret, isPending: isUpdating } = useSecretsCreateMutation({
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(`Successfully updated secret "${variables.secrets[0].name}"`)
|
||||
@@ -75,25 +66,83 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
confirmOnClose,
|
||||
handleOpenChange,
|
||||
modalProps: closeConfirmationModalProps,
|
||||
} = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.reset({ name: secretName.current ?? '', value: '' })
|
||||
}
|
||||
}, [form, secretName, visible])
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
showClose={false}
|
||||
size={'default'}
|
||||
className={'!min-w-screen lg:!min-w-[600px] flex flex-col'}
|
||||
>
|
||||
<Header />
|
||||
<Separator />
|
||||
<FormBody form={form} onSubmit={onSubmit} />
|
||||
<SheetContent size="default" className={'!min-w-screen lg:!min-w-[600px] flex flex-col'}>
|
||||
<SheetHeader className="py-3 flex flex-row gap-3 items-center">
|
||||
<SheetTitle>Edit secret</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<SheetSection className="h-full">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Name" layout="horizontal">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
readOnly
|
||||
className="!text-foreground-light cursor-not-allowed"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Value"
|
||||
layout="horizontal"
|
||||
description="Secrets can’t be retrieved once saved. Enter a new value to overwrite the existing value."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
type={showSecretValue ? 'text' : 'password'}
|
||||
placeholder="my-secret-value"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-form-type="other"
|
||||
data-bwignore
|
||||
actions={
|
||||
<div className="mr-1">
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={showSecretValue ? <EyeOff /> : <Eye />}
|
||||
onClick={() => setShowSecretValue(!showSecretValue)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter>
|
||||
<Button disabled={isUpdating} type="default" onClick={confirmOnClose}>
|
||||
Cancel
|
||||
@@ -103,116 +152,8 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
<DiscardChangesConfirmationDialog {...closeConfirmationModalProps} />
|
||||
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = (): ReactNode => {
|
||||
return (
|
||||
<SheetHeader className="py-3 flex flex-row gap-3 items-center border-b-0">
|
||||
<SheetClose
|
||||
className={cn(
|
||||
'text-muted hover:text ring-offset-background hover:opacity-100',
|
||||
'focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none data-[state=open]:bg-secondary',
|
||||
'transition'
|
||||
)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose>
|
||||
<SheetTitle>Edit secret</SheetTitle>
|
||||
</SheetHeader>
|
||||
)
|
||||
}
|
||||
|
||||
type FormBodyProps = {
|
||||
form: UseFormReturn<FormSchemaType>
|
||||
onSubmit: SubmitHandler<FormSchemaType>
|
||||
}
|
||||
|
||||
const FormBody = ({ form, onSubmit }: FormBodyProps): ReactNode => {
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={FORM_ID} className="flex-grow overflow-auto" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SheetSection>
|
||||
<NameField form={form} />
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<SheetSection className="space-y-4">
|
||||
<SecretField form={form} />
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
type NameFieldProps = {
|
||||
form: UseFormReturn<FormSchemaType>
|
||||
}
|
||||
|
||||
const NameField = ({ form }: NameFieldProps): ReactNode => {
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Name" layout="horizontal">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
readOnly
|
||||
className="!text-foreground-light cursor-not-allowed"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type SecretFieldProps = {
|
||||
form: UseFormReturn<FormSchemaType>
|
||||
}
|
||||
|
||||
const SecretField = ({ form }: SecretFieldProps): ReactNode => {
|
||||
const [showSecretValue, setShowSecretValue] = useState(false)
|
||||
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Value"
|
||||
layout="horizontal"
|
||||
description="Secrets can’t be retrieved once saved. Enter a new value to overwrite the existing value."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
type={showSecretValue ? 'text' : 'password'}
|
||||
placeholder="my-secret-value"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-form-type="other"
|
||||
data-bwignore
|
||||
actions={
|
||||
<div className="mr-1">
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={showSecretValue ? <EyeOff /> : <Eye />}
|
||||
onClick={() => setShowSecretValue(!showSecretValue)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+239
-224
@@ -18,13 +18,15 @@ import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
RadioGroupStacked,
|
||||
RadioGroupStackedItem,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
@@ -43,18 +45,19 @@ import { HttpRequestSection } from '../HttpRequestSection'
|
||||
import { SqlFunctionSection } from '../SqlFunctionSection'
|
||||
import { SqlSnippetSection } from '../SqlSnippetSection'
|
||||
import {
|
||||
FormSchema,
|
||||
type CreateCronJobForm,
|
||||
type CronJobType,
|
||||
FormSchema,
|
||||
} from './CreateCronJobSheet.constants'
|
||||
import { CronJobScheduleSection } from './CronJobScheduleSection'
|
||||
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
|
||||
import { isGreaterThanOrEqual } from '@/lib/semver'
|
||||
|
||||
interface CreateCronJobSheetProps {
|
||||
open: boolean
|
||||
selectedCronJob?: Pick<CronJob, 'jobname' | 'schedule' | 'active' | 'command'>
|
||||
supportsSeconds: boolean
|
||||
onDirty: (isDirty: boolean) => void
|
||||
onClose: () => void
|
||||
onCloseWithConfirmation: () => void
|
||||
}
|
||||
|
||||
const FORM_ID = 'create-cron-job-sidepanel'
|
||||
@@ -83,13 +86,7 @@ const buildCommand = (values: CronJobType) => {
|
||||
return command
|
||||
}
|
||||
|
||||
export const CreateCronJobSheet = ({
|
||||
selectedCronJob,
|
||||
supportsSeconds,
|
||||
onDirty,
|
||||
onClose,
|
||||
onCloseWithConfirmation: confirmOnClose,
|
||||
}: CreateCronJobSheetProps) => {
|
||||
export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCronJobSheetProps) => {
|
||||
const { childId } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
@@ -100,13 +97,18 @@ export const CreateCronJobSheet = ({
|
||||
const isEditing = !!selectedCronJob?.jobname
|
||||
const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false)
|
||||
|
||||
const { data } = useDatabaseExtensionsQuery({
|
||||
const { data = [] } = useDatabaseExtensionsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const pgNetExtension = (data ?? []).find((ext) => ext.name === 'pg_net')
|
||||
const pgNetExtension = data.find((ext) => ext.name === 'pg_net')
|
||||
const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined
|
||||
|
||||
const pgCronExtension = data.find((ext) => ext.name === 'pg_cron')
|
||||
const supportsSeconds = pgCronExtension?.installed_version
|
||||
? isGreaterThanOrEqual(pgCronExtension.installed_version, '1.5')
|
||||
: false
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { mutate: upsertCronJob, isPending: isUpserting } = useDatabaseCronJobCreateMutation()
|
||||
const isLoading = isLoadingGetCronJob || isUpserting
|
||||
@@ -118,25 +120,18 @@ export const CreateCronJobSheet = ({
|
||||
|
||||
const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!)
|
||||
|
||||
const defaultValues = {
|
||||
name: selectedCronJob?.jobname || '',
|
||||
schedule: selectedCronJob?.schedule || '*/5 * * * *',
|
||||
supportsSeconds,
|
||||
values: cronJobValues,
|
||||
}
|
||||
|
||||
const form = useForm<CreateCronJobForm>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: selectedCronJob?.jobname || '',
|
||||
schedule: selectedCronJob?.schedule || '*/5 * * * *',
|
||||
supportsSeconds,
|
||||
values: cronJobValues,
|
||||
},
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch(() => {
|
||||
const isDirty = form.formState.isDirty
|
||||
onDirty(isDirty)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [form, onDirty])
|
||||
|
||||
const [
|
||||
cronType,
|
||||
endpoint,
|
||||
@@ -162,36 +157,10 @@ export const CreateCronJobSheet = ({
|
||||
],
|
||||
})
|
||||
|
||||
// update the snippet field when the user changes the any values in the form
|
||||
useEffect(() => {
|
||||
const command = buildCommand({
|
||||
type: cronType,
|
||||
method,
|
||||
edgeFunctionName,
|
||||
timeoutMs,
|
||||
httpHeaders,
|
||||
httpBody,
|
||||
functionName,
|
||||
schema,
|
||||
endpoint,
|
||||
snippet: '',
|
||||
})
|
||||
if (command) {
|
||||
form.setValue('values.snippet', command)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
edgeFunctionName,
|
||||
endpoint,
|
||||
method,
|
||||
// for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same
|
||||
JSON.stringify(httpHeaders),
|
||||
httpBody,
|
||||
timeoutMs,
|
||||
schema,
|
||||
functionName,
|
||||
form,
|
||||
])
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => form.formState.isDirty,
|
||||
onClose: () => onClose(),
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<CreateCronJobForm> = async ({ name, schedule, values }) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
@@ -272,177 +241,223 @@ export const CreateCronJobSheet = ({
|
||||
setIsLoadingGetCronJob(false)
|
||||
}
|
||||
|
||||
// update the snippet field when the user changes the any values in the form
|
||||
useEffect(() => {
|
||||
const command = buildCommand({
|
||||
type: cronType,
|
||||
method,
|
||||
edgeFunctionName,
|
||||
timeoutMs,
|
||||
httpHeaders,
|
||||
httpBody,
|
||||
functionName,
|
||||
schema,
|
||||
endpoint,
|
||||
snippet: '',
|
||||
})
|
||||
if (command) {
|
||||
form.setValue('values.snippet', command)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
cronType,
|
||||
edgeFunctionName,
|
||||
endpoint,
|
||||
method,
|
||||
// for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same
|
||||
JSON.stringify(httpHeaders),
|
||||
httpBody,
|
||||
timeoutMs,
|
||||
schema,
|
||||
functionName,
|
||||
form,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !!pgCronExtension) form.reset(defaultValues)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
|
||||
<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} disabled={isEditing} />
|
||||
</FormControl_Shadcn_>
|
||||
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
|
||||
Cron jobs cannot be renamed once created
|
||||
</span>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<CronJobScheduleSection form={form} supportsSeconds={supportsSeconds} />
|
||||
<Separator />
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.type"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Type" layout="vertical" className="gap-1">
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked
|
||||
id="function_type"
|
||||
name="function_type"
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
{CRONJOB_DEFINITIONS.map((definition) => (
|
||||
<RadioGroupStackedItem
|
||||
key={definition.value}
|
||||
id={definition.value}
|
||||
value={definition.value}
|
||||
disabled={
|
||||
!pgNetExtensionInstalled &&
|
||||
(definition.value === 'http_request' ||
|
||||
definition.value === 'edge_function')
|
||||
}
|
||||
label=""
|
||||
showIndicator={false}
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="lg">
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
|
||||
</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} disabled={isEditing} />
|
||||
</FormControl_Shadcn_>
|
||||
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
|
||||
Cron jobs cannot be renamed once created
|
||||
</span>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
<CronJobScheduleSection form={form} supportsSeconds={supportsSeconds} />
|
||||
<Separator />
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="values.type"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Type" layout="vertical" className="gap-1">
|
||||
<FormControl_Shadcn_>
|
||||
<RadioGroupStacked
|
||||
id="function_type"
|
||||
name="function_type"
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
<div className="flex items-center gap-x-5">
|
||||
<div className="text-foreground">{definition.icon}</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-x-2">
|
||||
<p className="text-foreground">{definition.label}</p>
|
||||
{CRONJOB_DEFINITIONS.map((definition) => (
|
||||
<RadioGroupStackedItem
|
||||
key={definition.value}
|
||||
id={definition.value}
|
||||
value={definition.value}
|
||||
disabled={
|
||||
!pgNetExtensionInstalled &&
|
||||
(definition.value === 'http_request' ||
|
||||
definition.value === 'edge_function')
|
||||
}
|
||||
label=""
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex items-center gap-x-5">
|
||||
<div className="text-foreground">{definition.icon}</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-x-2">
|
||||
<p className="text-foreground">{definition.label}</p>
|
||||
</div>
|
||||
<p className="text-foreground-light">
|
||||
{definition.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-foreground-light">{definition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!pgNetExtensionInstalled &&
|
||||
(definition.value === 'http_request' ||
|
||||
definition.value === 'edge_function') ? (
|
||||
<div className="w-full flex gap-x-2 pl-11 py-2 items-center">
|
||||
<WarningIcon />
|
||||
<span className="text-xs">
|
||||
<code>pg_net</code> needs to be installed to use this type
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</RadioGroupStackedItem>
|
||||
))}
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
{!pgNetExtensionInstalled &&
|
||||
(definition.value === 'http_request' ||
|
||||
definition.value === 'edge_function') ? (
|
||||
<div className="w-full flex gap-x-2 pl-11 py-2 items-center">
|
||||
<WarningIcon />
|
||||
<span className="text-xs">
|
||||
<code>pg_net</code> needs to be installed to use this type
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</RadioGroupStackedItem>
|
||||
))}
|
||||
</RadioGroupStacked>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{!pgNetExtensionInstalled && (
|
||||
<Admonition
|
||||
type="note"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<span>
|
||||
Enable <code className="text-code-inline w-min">pg_net</code> for HTTP
|
||||
requests or Edge Functions
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<span>
|
||||
This will allow you to send HTTP requests or trigger an edge function
|
||||
within your cron jobs
|
||||
</span>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
className="w-min"
|
||||
disabled={!canToggleExtensions}
|
||||
onClick={() => setShowEnableExtensionModal(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canToggleExtensions
|
||||
? 'You need additional permissions to enable database extensions'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Install pg_net extension
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
{cronType === 'http_request' && (
|
||||
<>
|
||||
<HttpRequestSection form={form} />
|
||||
<Separator />
|
||||
<HTTPHeaderFieldsSection variant={cronType} />
|
||||
<Separator />
|
||||
<HttpBodyFieldSection form={form} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{!pgNetExtensionInstalled && (
|
||||
<Admonition
|
||||
type="note"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<span>
|
||||
Enable <code className="text-code-inline w-min">pg_net</code> for HTTP
|
||||
requests or Edge Functions
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<span>
|
||||
This will allow you to send HTTP requests or trigger an edge function
|
||||
within your cron jobs
|
||||
</span>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
className="w-min"
|
||||
disabled={!canToggleExtensions}
|
||||
onClick={() => setShowEnableExtensionModal(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canToggleExtensions
|
||||
? 'You need additional permissions to enable database extensions'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Install pg_net extension
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SheetSection>
|
||||
<Separator />
|
||||
{cronType === 'http_request' && (
|
||||
<>
|
||||
<HttpRequestSection form={form} />
|
||||
<Separator />
|
||||
<HTTPHeaderFieldsSection variant={cronType} />
|
||||
<Separator />
|
||||
<HttpBodyFieldSection form={form} />
|
||||
</>
|
||||
)}
|
||||
{cronType === 'edge_function' && (
|
||||
<>
|
||||
<EdgeFunctionSection form={form} />
|
||||
<Separator />
|
||||
<HTTPHeaderFieldsSection variant={cronType} />
|
||||
<Separator />
|
||||
<HttpBodyFieldSection form={form} />
|
||||
</>
|
||||
)}
|
||||
{cronType === 'sql_function' && <SqlFunctionSection form={form} />}
|
||||
{cronType === 'sql_snippet' && <SqlSnippetSection form={form} />}
|
||||
</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}
|
||||
>
|
||||
{isEditing ? `Save cron job` : 'Create cron job'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
{cronType === 'edge_function' && (
|
||||
<>
|
||||
<EdgeFunctionSection form={form} />
|
||||
<Separator />
|
||||
<HTTPHeaderFieldsSection variant={cronType} />
|
||||
<Separator />
|
||||
<HttpBodyFieldSection form={form} />
|
||||
</>
|
||||
)}
|
||||
{cronType === 'sql_function' && <SqlFunctionSection form={form} />}
|
||||
{cronType === 'sql_snippet' && <SqlSnippetSection form={form} />}
|
||||
</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}
|
||||
>
|
||||
{isEditing ? `Save cron job` : 'Create cron job'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{pgNetExtension && (
|
||||
<EnableExtensionModal
|
||||
visible={showEnableExtensionModal}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { toString as CronToString } from 'cronstrue'
|
||||
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 { type ConfirmOnCloseModalProps, useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Edit3, List } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -15,15 +14,12 @@ import {
|
||||
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
|
||||
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
|
||||
Button,
|
||||
cn,
|
||||
CodeBlock,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderAside,
|
||||
@@ -64,15 +60,6 @@ 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 pageTitle = childLabel || childId || 'Cron Job'
|
||||
|
||||
const pageSubtitle = job ? (
|
||||
@@ -192,40 +179,13 @@ export const CronJobPage = () => {
|
||||
|
||||
<PreviousRunsTab />
|
||||
|
||||
<Sheet open={isEditSheetOpen} onOpenChange={setIsEditSheetOpen}>
|
||||
<SheetContent size="lg">
|
||||
{job && (
|
||||
<CreateCronJobSheet
|
||||
selectedCronJob={{
|
||||
jobname: job.jobname,
|
||||
schedule: job.schedule,
|
||||
active: job.active,
|
||||
command: job.command,
|
||||
}}
|
||||
supportsSeconds={true}
|
||||
onDirty={setIsDirty}
|
||||
onClose={() => setIsEditSheetOpen(false)}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
{job && (
|
||||
<CreateCronJobSheet
|
||||
open={isEditSheetOpen}
|
||||
selectedCronJob={job}
|
||||
onClose={() => setIsEditSheetOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
@@ -5,7 +5,6 @@ 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 { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
|
||||
import { createNavigationHandler } from 'lib/navigation'
|
||||
import { isGreaterThanOrEqual } from 'lib/semver'
|
||||
@@ -14,8 +13,7 @@ import { useRouter } from 'next/router'
|
||||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
|
||||
import { MouseEvent, UIEvent, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { LoadingLine, Sheet, SheetContent } from 'ui'
|
||||
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { LoadingLine } from 'ui'
|
||||
|
||||
import { formatCronJobColumns } from './CronJobs.utils'
|
||||
import { CronJobRunDetailsOverflowNotice } from './CronJobsTab.CleanupNotice'
|
||||
@@ -34,7 +32,6 @@ export const CronjobsTab = () => {
|
||||
|
||||
const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault(''))
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const [search, setSearch] = useState(searchQuery)
|
||||
|
||||
const handleSearchSubmit = () => {
|
||||
@@ -142,13 +139,6 @@ export const CronjobsTab = () => {
|
||||
setCreateCronJobSheetShown(false)
|
||||
cleanPointerEventsNoneOnBody(500)
|
||||
}
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setIsDirty(false)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (grid.isSuccess && !!cronJobIdForEditing && !cronJobForEditing) {
|
||||
@@ -192,18 +182,11 @@ export const CronjobsTab = () => {
|
||||
|
||||
<DeleteCronJob />
|
||||
|
||||
<Sheet open={!!createCronJobSheetShown || !!cronJobForEditing} onOpenChange={confirmOnClose}>
|
||||
<SheetContent size="default" tabIndex={undefined}>
|
||||
<CreateCronJobSheet
|
||||
selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
|
||||
supportsSeconds={supportsSeconds}
|
||||
onDirty={setIsDirty}
|
||||
onClose={onClose}
|
||||
onCloseWithConfirmation={confirmOnClose}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<CreateCronJobSheet
|
||||
open={!!createCronJobSheetShown || !!cronJobForEditing}
|
||||
selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -228,19 +211,3 @@ const CronJobsFooter = ({ count }: CronJobsFooterProps) => (
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Confirmation modal for unsaved changes
|
||||
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,7 @@ 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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { QUEUE_TYPES } from './Queues.constants'
|
||||
import { QueryNameSchema } from './Queues.utils'
|
||||
@@ -102,7 +102,7 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) =>
|
||||
|
||||
const checkIsDirty = () => form.formState.isDirty
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
})
|
||||
@@ -144,7 +144,7 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) =>
|
||||
const queueType = form.watch('values.type')
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={visible} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="default" className="w-[35%]" tabIndex={undefined}>
|
||||
<div className="flex flex-col h-full" tabIndex={-1}>
|
||||
<SheetHeader>
|
||||
@@ -322,23 +322,8 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) =>
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</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>
|
||||
)
|
||||
@@ -11,8 +11,9 @@ import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
|
||||
import { FDW } from 'data/fdw/fdws-query'
|
||||
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import InputField from './InputField'
|
||||
import { WrapperMeta } from './Wrappers.types'
|
||||
@@ -108,7 +109,7 @@ export const EditWrapperSheet = ({
|
||||
|
||||
const checkIsDirty = useCallback(() => hasChangesRef.current, [])
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty,
|
||||
onClose,
|
||||
})
|
||||
@@ -397,7 +398,7 @@ export const EditWrapperSheet = ({
|
||||
<p className="text-sm text-foreground-light mt-2">Are you sure you want to continue?</p>
|
||||
</ConfirmationModal>
|
||||
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
@@ -412,18 +413,3 @@ 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>
|
||||
)
|
||||
@@ -5,10 +5,11 @@ import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
@@ -19,7 +20,6 @@ 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'
|
||||
@@ -45,7 +45,7 @@ export const WrapperOverviewTab = () => {
|
||||
})
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isDirty,
|
||||
onClose: () => {
|
||||
setCreateWrapperShown(false)
|
||||
@@ -141,7 +141,7 @@ export const WrapperOverviewTab = () => {
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="lg" tabIndex={undefined}>
|
||||
<CreateWrapperSheetComponent
|
||||
wrapperMeta={wrapperMeta}
|
||||
@@ -153,22 +153,7 @@ export const WrapperOverviewTab = () => {
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</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>
|
||||
)
|
||||
@@ -4,10 +4,10 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { 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 { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { HTMLProps, ReactNode, useCallback, useState } from 'react'
|
||||
import { Sheet, SheetContent } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
|
||||
import { CreateWrapperSheet } from './CreateWrapperSheet'
|
||||
import { WRAPPERS } from './Wrappers.constants'
|
||||
@@ -38,7 +38,7 @@ export const WrappersTab = () => {
|
||||
: []
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: useCallback(() => isDirty, [isDirty]),
|
||||
onClose: useCallback(() => {
|
||||
setCreateWrapperShown(false)
|
||||
@@ -50,7 +50,7 @@ export const WrappersTab = () => {
|
||||
({ ...props }: { children: ReactNode } & HTMLProps<HTMLDivElement>) => (
|
||||
<div className="w-full mx-10 py-10 ">
|
||||
{props.children}
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
|
||||
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
|
||||
<SheetContent size="lg" tabIndex={undefined}>
|
||||
{wrapperMeta && (
|
||||
<CreateWrapperSheet
|
||||
@@ -64,7 +64,7 @@ export const WrappersTab = () => {
|
||||
</Sheet>
|
||||
</div>
|
||||
),
|
||||
[createWrapperShown, wrapperMeta, confirmOnClose]
|
||||
[createWrapperShown, handleOpenChange, wrapperMeta, confirmOnClose]
|
||||
)
|
||||
|
||||
if (!wrapperMeta) {
|
||||
@@ -102,22 +102,7 @@ export const WrappersTab = () => {
|
||||
return (
|
||||
<Container>
|
||||
<WrapperTable />
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</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>
|
||||
)
|
||||
@@ -1,11 +1,6 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
import { useParams } from 'common'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import PolicyEditorModal from 'components/interfaces/Auth/Policies/PolicyEditorModal'
|
||||
import { PolicyEditorModal } from 'components/interfaces/Auth/Policies/PolicyEditorModal'
|
||||
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
||||
import { useDatabasePolicyCreateMutation } from 'data/database-policies/database-policy-create-mutation'
|
||||
import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation'
|
||||
@@ -13,8 +8,12 @@ import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database
|
||||
import { usePaginatedBucketsQuery } from 'data/storage/buckets-query'
|
||||
import { useDebouncedValue } from 'hooks/misc/useDebouncedValue'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { PageContainer } from 'ui-patterns/PageContainer'
|
||||
import {
|
||||
PageSection,
|
||||
@@ -24,10 +23,11 @@ import {
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
|
||||
import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.utils'
|
||||
import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
|
||||
import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection'
|
||||
import StoragePoliciesEditPolicyModal from './StoragePoliciesEditPolicyModal'
|
||||
import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal'
|
||||
|
||||
export const StoragePolicies = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
+5
-7
@@ -1,14 +1,14 @@
|
||||
import { noop, pull } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { POLICY_MODAL_VIEWS } from 'components/interfaces/Auth/Policies/Policies.constants'
|
||||
import PolicySelection from 'components/interfaces/Auth/Policies/PolicySelection'
|
||||
import PolicyTemplates from 'components/interfaces/Auth/Policies/PolicyTemplates'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { noop, pull } from 'lodash'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Modal } from 'ui'
|
||||
|
||||
import {
|
||||
applyBucketIdToTemplateDefinition,
|
||||
createPayloadsForAddPolicy,
|
||||
@@ -26,7 +26,7 @@ const newPolicyTemplate: any = {
|
||||
allowedOperations: [],
|
||||
}
|
||||
|
||||
const StoragePoliciesEditPolicyModal = ({
|
||||
export const StoragePoliciesEditPolicyModal = ({
|
||||
visible = false,
|
||||
bucketName = '',
|
||||
onSelectCancel = () => {},
|
||||
@@ -260,5 +260,3 @@ const StoragePoliciesEditPolicyModal = ({
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default StoragePoliciesEditPolicyModal
|
||||
+4
-19
@@ -29,7 +29,7 @@ import { getTables } from 'data/tables/tables-query'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { isValidExperimentVariant } from 'hooks/misc/useTableCreateGeneratePolicies'
|
||||
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
|
||||
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
|
||||
import { usePHFlag } from 'hooks/ui/useFlag'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { useTrack } from 'lib/telemetry/track'
|
||||
@@ -41,7 +41,7 @@ import { useTableEditorStateSnapshot, type TableEditorState } from 'state/table-
|
||||
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
|
||||
import type { Dictionary } from 'types'
|
||||
import { SonnerProgress } from 'ui'
|
||||
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
|
||||
import { ColumnEditor } from './ColumnEditor/ColumnEditor'
|
||||
import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types'
|
||||
@@ -229,7 +229,7 @@ export const SidePanelEditor = ({
|
||||
}
|
||||
)
|
||||
|
||||
const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({
|
||||
const { confirmOnClose, modalProps } = useConfirmOnClose({
|
||||
checkIsDirty: () => isEdited,
|
||||
onClose: () => {
|
||||
setIsEdited(false)
|
||||
@@ -1011,22 +1011,7 @@ export const SidePanelEditor = ({
|
||||
visible={snap.sidePanel?.type === 'operation-queue'}
|
||||
closePanel={snap.closeSidePanel}
|
||||
/>
|
||||
<CloseConfirmationModal {...closeConfirmationModalProps} />
|
||||
<DiscardChangesConfirmationDialog {...modalProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
@@ -19,16 +19,18 @@ interface DiscardChangesConfirmationDialogProps extends ConfirmOnCloseModalProps
|
||||
description?: ReactNode
|
||||
confirmLabel?: ReactNode
|
||||
cancelLabel?: ReactNode
|
||||
size?: React.ComponentProps<typeof AlertDialogContent>['size']
|
||||
}
|
||||
|
||||
export const DiscardChangesConfirmationDialog = ({
|
||||
visible,
|
||||
onClose,
|
||||
onCancel,
|
||||
title = 'Discard changes?',
|
||||
description = 'Are you sure you want to discard your changes? Any unsaved changes will be lost.',
|
||||
title = 'Unsaved changes',
|
||||
description = 'You have unsaved changes. Are you sure you want to discard them?',
|
||||
confirmLabel = 'Discard changes',
|
||||
cancelLabel = 'Keep editing',
|
||||
size = 'tiny',
|
||||
}: DiscardChangesConfirmationDialogProps) => {
|
||||
const isConfirmingRef = useRef(false)
|
||||
|
||||
@@ -59,7 +61,7 @@ export const DiscardChangesConfirmationDialog = ({
|
||||
|
||||
return (
|
||||
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent size={size}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
{description !== undefined && description !== null && (
|
||||
|
||||
Reference in new issue
Block a user