refactor again

This commit is contained in:
Saxon Fletcher committed 2025-08-06 12:29:47 +10:00
1 parent 40708d2702
commit 00d91d0af6
5 files changed
+212 -139

No files matched your search

@@ -1,7 +1,7 @@
import { debounce } from 'lodash'
import { Book, Save, X } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useForm } from 'react-hook-form'
import { useParams } from 'common'
import {
@@ -13,7 +13,7 @@ import { SqlRunButton } from 'components/interfaces/SQLEditor/UtilityPanel/RunBu
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { BASE_PATH } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
@@ -28,9 +28,12 @@ import {
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
HoverCard_Shadcn_,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
Input_Shadcn_ as Input,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
@@ -39,6 +42,7 @@ import {
SheetHeader,
SheetTitle,
SQL_ICON,
SheetDescription,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils'
@@ -46,21 +50,45 @@ import AIEditor from '../AIEditor'
import { ButtonTooltip } from '../ButtonTooltip'
import { InlineLink } from '../InlineLink'
import SqlWarningAdmonition from '../SqlWarningAdmonition'
import type { EditorPanelProps } from './EditorPanel.types'
type Template = {
name: string
description: string
content: string
}
interface EditorPanelProps {
open: boolean
onClose: () => void
initialValue?: string
label?: string
saveLabel?: string
saveValue?: string
onSave?: (value: string) => void
onRunSuccess?: (value: any[]) => void
onRunError?: (value: any) => void
functionName?: string
templates?: Template[]
initialPrompt?: string
onChange?: (value: string) => void
}
export const EditorPanel = ({
open,
onClose,
initialValue = '',
label = 'SQL Editor',
saveLabel = 'Run',
label = '',
saveLabel = 'Save',
saveValue = '',
onSave,
onRunSuccess,
onRunError,
templates = [],
initialPrompt = '',
onChange,
}: EditorPanelProps) => {
const { ref } = useParams()
const project = useSelectedProject()
const { data: project } = useSelectedProjectQuery()
const { profile } = useProfile()
const snapV2 = useSqlEditorV2StateSnapshot()
const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation()
@@ -71,11 +99,15 @@ export const EditorPanel = ({
const [results, setResults] = useState<undefined | any[]>(undefined)
const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>()
const [currentValue, setCurrentValue] = useState(initialValue)
const [savedCode, setSavedCode] = useState<string>('')
const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false)
const [showResults, setShowResults] = useState(true)
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false)
const saveForm = useForm({
defaultValues: {
saveValue: saveValue || '',
},
})
const errorHeader = error?.formattedError?.split('\n')?.filter((x: string) => x.length > 0)?.[0]
const errorContent =
error?.formattedError
@@ -86,14 +118,20 @@ export const EditorPanel = ({
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
onSuccess: async (res) => {
setResults(res.result)
if (onRunSuccess) {
onRunSuccess(res.result)
}
},
onError: (error) => {
setError(error)
setResults([])
if (onRunError) {
onRunError(error)
}
},
})
const onExecuteSql = (skipValidation = false) => {
const onExecuteSql = async (skipValidation = false) => {
setError(undefined)
setShowWarning(undefined)
@@ -107,11 +145,6 @@ export const EditorPanel = ({
return
}
}
if (onSave) {
onSave(currentValue)
}
executeSql({
sql: suffixWithLimit(currentValue, 100),
projectRef: project?.ref,
@@ -133,28 +166,21 @@ export const EditorPanel = ({
setIsTemplatesOpen(false)
}
// Create a debounced version of the revert code function
const debouncedRevertCode = useCallback(
debounce(() => {
setIsPreviewingTemplate(false)
handleChange(savedCode)
}, 300),
[savedCode]
)
// Cleanup debounce on unmount
useEffect(() => {
return () => {
debouncedRevertCode.cancel()
}
}, [debouncedRevertCode])
useEffect(() => {
if (initialValue !== undefined && initialValue !== currentValue) {
setCurrentValue(initialValue)
setResults(undefined)
setError(undefined)
setShowWarning(undefined)
}
}, [initialValue])
useEffect(() => {
saveForm.reset({
saveValue: saveValue || '',
})
}, [saveValue, saveForm])
return (
<Sheet open={open} onOpenChange={(open) => !open && onClose()}>
<SheetContent
@@ -162,7 +188,10 @@ export const EditorPanel = ({
className="w-full sm:max-w-3xl flex flex-col h-full p-0 space-y-0 gap-0"
>
<SheetHeader className="flex shrink-0 items-center gap-x-3">
<SheetTitle className="text-sm flex-1">{label}</SheetTitle>
<div className="flex-1">
<SheetTitle className="text-sm">SQL Editor</SheetTitle>
{label && <SheetDescription className="text-sm">{label}</SheetDescription>}
</div>
<div className="flex gap-2 items-center">
{templates.length > 0 && (
<Popover_Shadcn_ open={isTemplatesOpen} onOpenChange={setIsTemplatesOpen}>
@@ -371,6 +400,34 @@ export const EditorPanel = ({
)}
<div className="flex items-center gap-2 justify-end px-5 py-4 w-full border-t shrink-0">
{onSave && (
<Form_Shadcn_ {...saveForm}>
<form
onSubmit={saveForm.handleSubmit((values) => {
onSave(values.saveValue)
})}
className="flex items-center gap-2"
>
{saveValue && (
<FormField_Shadcn_
control={saveForm.control}
name="saveValue"
render={({ field }) => (
<Input size="tiny" placeholder="Enter save value..." {...field} />
)}
/>
)}
<Button
size="tiny"
type="default"
htmlType="submit"
disabled={!saveForm.formState.isDirty}
>
{saveLabel}
</Button>
</form>
</Form_Shadcn_>
)}
<SqlRunButton
isDisabled={isExecuting}
isExecuting={isExecuting}
@@ -1,27 +0,0 @@
export type Template = {
name: string
description: string
content: string
}
export interface EditorPanelProps {
open: boolean
onClose: () => void
initialValue?: string
label?: string
saveLabel?: string
onSave?: (value: string) => void
functionName?: string
templates?: Template[]
initialPrompt?: string
onChange?: (value: string) => void
}
export interface EditorPanelState {
initialValue?: string
label?: string
saveLabel?: string
templates?: Template[]
initialPrompt?: string
onSave?: (value: string) => void
}
@@ -14,7 +14,6 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel'
import type { EditorPanelState } from 'components/ui/EditorPanel/EditorPanel.types'
import NoPermission from 'components/ui/NoPermission'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
@@ -73,10 +72,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
const [selectedTable, setSelectedTable] = useState<string>()
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<PostgresPolicy>()
// Local editor panel state
const [editorPanelOpen, setEditorPanelOpen] = useState(false)
const [editorPanelProps, setEditorPanelProps] = useState<EditorPanelState>({})
const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
@@ -146,43 +144,20 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
hasTables={tables.length > 0}
isLocked={isSchemaLocked}
onSelectCreatePolicy={(table: string) => {
setSelectedTable(table)
setSelectedPolicyToEdit(undefined)
if (isInlineEditorEnabled) {
setEditorPanelProps({
initialValue: `create policy "replace_with_policy_name"
on ${schema}.${table}
for select
to authenticated
using (
true -- Write your policy condition here
);`,
label: `Create new RLS policy on "${table}"`,
saveLabel: 'Create policy',
initialPrompt: `Create and name a entirely new RLS policy for the "${table}" table in the ${schema} schema. The policy should...`,
})
setEditorPanelOpen(true)
} else {
setSelectedTable(table)
setShowPolicyAiEditor(true)
}
}}
onSelectEditPolicy={(policy) => {
setSelectedPolicyToEdit(policy)
setSelectedTable(undefined)
if (isInlineEditorEnabled) {
const sql = generatePolicyUpdateSQL(policy)
const templates = getGeneralPolicyTemplates(policy.schema, policy.table)
setEditorPanelProps({
initialValue: sql,
label: `Edit policy "${policy.name}"`,
saveLabel: 'Update policy',
templates: templates.map((template) => ({
name: template.templateName,
description: template.description,
content: template.statement,
})),
initialPrompt: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
})
setEditorPanelOpen(true)
} else {
setSelectedPolicyToEdit(policy)
setShowPolicyAiEditor(true)
}
}}
@@ -202,11 +177,58 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
}}
authContext="database"
/>
<EditorPanel
open={editorPanelOpen}
onClose={() => setEditorPanelOpen(false)}
{...editorPanelProps}
onClose={() => {
setEditorPanelOpen(false)
setSelectedPolicyToEdit(undefined)
setSelectedTable(undefined)
}}
onRunSuccess={() => {
setEditorPanelOpen(false)
setSelectedPolicyToEdit(undefined)
setSelectedTable(undefined)
}}
initialValue={
selectedPolicyToEdit
? generatePolicyUpdateSQL(selectedPolicyToEdit)
: selectedTable
? `create policy "replace_with_policy_name"
on ${schema}.${selectedTable}
for select
to authenticated
using (
true -- Write your policy condition here
);`
: ''
}
label={
selectedPolicyToEdit
? 'RLS policies are just SQL statements that you can alter'
: selectedTable
? `Create new RLS policy on "${selectedTable}"`
: ''
}
initialPrompt={
selectedPolicyToEdit
? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...`
: selectedTable
? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...`
: ''
}
templates={
selectedPolicyToEdit
? getGeneralPolicyTemplates(
selectedPolicyToEdit.schema,
selectedPolicyToEdit.table
).map((template) => ({
name: template.templateName,
description: template.description,
content: template.statement,
}))
: []
}
/>
</div>
)
@@ -8,7 +8,6 @@ import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel'
import type { EditorPanelState } from 'components/ui/EditorPanel/EditorPanel.types'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import NoPermission from 'components/ui/NoPermission'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
@@ -20,29 +19,19 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => {
const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false)
const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false)
const isInlineEditorEnabled = useIsInlineEditorEnabled()
// Local editor panel state
const [editorPanelOpen, setEditorPanelOpen] = useState(false)
const [editorPanelProps, setEditorPanelProps] = useState<EditorPanelState>({})
const [selectedFunctionForEditor, setSelectedFunctionForEditor] = useState<
DatabaseFunction | undefined
>()
const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions')
const isPermissionsLoaded = usePermissionsLoaded()
const createFunction = () => {
if (isInlineEditorEnabled) {
setEditorPanelProps({
initialValue: `create function function_name()
returns void
language plpgsql
as $$
begin
-- Write your function logic here
end;
$$;`,
label: 'Create new database function',
saveLabel: 'Create function',
initialPrompt: 'Create a new database function that...',
})
setSelectedFunctionForEditor(undefined)
setEditorPanelOpen(true)
} else {
setSelectedFunction(undefined)
@@ -52,12 +41,7 @@ $$;`,
const editFunction = (fn: DatabaseFunction) => {
if (isInlineEditorEnabled) {
setEditorPanelProps({
initialValue: fn.complete_statement,
label: `Edit function "${fn.name}"`,
saveLabel: 'Update function',
initialPrompt: `Update the database function "${fn.name}" to...`,
})
setSelectedFunctionForEditor(fn)
setEditorPanelOpen(true)
} else {
setSelectedFunction(fn)
@@ -101,11 +85,39 @@ $$;`,
visible={showDeleteFunctionForm}
setVisible={setShowDeleteFunctionForm}
/>
<EditorPanel
open={editorPanelOpen}
onClose={() => setEditorPanelOpen(false)}
{...editorPanelProps}
onRunSuccess={() => {
setEditorPanelOpen(false)
setSelectedFunctionForEditor(undefined)
}}
onClose={() => {
setEditorPanelOpen(false)
setSelectedFunctionForEditor(undefined)
}}
initialValue={
selectedFunctionForEditor
? selectedFunctionForEditor.complete_statement
: `create function function_name()
returns void
language plpgsql
as $$
begin
-- Write your function logic here
end;
$$;`
}
label={
selectedFunctionForEditor
? `Edit function "${selectedFunctionForEditor.name}"`
: 'Create new database function'
}
initialPrompt={
selectedFunctionForEditor
? `Update the database function "${selectedFunctionForEditor.name}" to...`
: 'Create a new database function that...'
}
/>
</>
)
@@ -11,7 +11,6 @@ import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel'
import type { EditorPanelState } from 'components/ui/EditorPanel/EditorPanel.types'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
@@ -23,25 +22,17 @@ const TriggersPage: NextPageWithLayout = () => {
const [selectedTrigger, setSelectedTrigger] = useState<PostgresTrigger>()
const [showCreateTriggerForm, setShowCreateTriggerForm] = useState<boolean>(false)
const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState<boolean>(false)
// Local editor panel state
const [editorPanelOpen, setEditorPanelOpen] = useState(false)
const [editorPanelProps, setEditorPanelProps] = useState<EditorPanelState>({})
const [selectedTriggerForEditor, setSelectedTriggerForEditor] = useState<PostgresTrigger>()
const canReadTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers')
const isPermissionsLoaded = usePermissionsLoaded()
const createTrigger = () => {
if (isInlineEditorEnabled) {
setEditorPanelProps({
initialValue: `create trigger trigger_name
after insert or update or delete on table_name
for each row
execute function function_name();`,
label: 'Create new database trigger',
saveLabel: 'Create trigger',
initialPrompt: 'Create a new database trigger that...',
})
setSelectedTriggerForEditor(undefined)
setEditorPanelOpen(true)
} else {
setSelectedTrigger(undefined)
@@ -51,13 +42,7 @@ execute function function_name();`,
const editTrigger = (trigger: PostgresTrigger) => {
if (isInlineEditorEnabled) {
const sql = generateTriggerCreateSQL(trigger)
setEditorPanelProps({
initialValue: sql,
label: `Edit trigger "${trigger.name}"`,
saveLabel: 'Update trigger',
initialPrompt: `Update the database trigger "${trigger.name}" to...`,
})
setSelectedTriggerForEditor(trigger)
setEditorPanelOpen(true)
} else {
setSelectedTrigger(trigger)
@@ -102,11 +87,35 @@ execute function function_name();`,
visible={showDeleteTriggerForm}
setVisible={setShowDeleteTriggerForm}
/>
<EditorPanel
open={editorPanelOpen}
onClose={() => setEditorPanelOpen(false)}
{...editorPanelProps}
onRunSuccess={() => {
setEditorPanelOpen(false)
setSelectedTriggerForEditor(undefined)
}}
onClose={() => {
setEditorPanelOpen(false)
setSelectedTriggerForEditor(undefined)
}}
initialValue={
selectedTriggerForEditor
? generateTriggerCreateSQL(selectedTriggerForEditor)
: `create trigger trigger_name
after insert or update or delete on table_name
for each row
execute function function_name();`
}
label={
selectedTriggerForEditor
? `Edit trigger "${selectedTriggerForEditor.name}"`
: 'Create new database trigger'
}
initialPrompt={
selectedTriggerForEditor
? `Update the database trigger "${selectedTriggerForEditor.name}" to...`
: 'Create a new database trigger that...'
}
/>
</>
)