From 00d91d0af6d4d8a62f09bc11b760eb603efe87ec Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 6 Aug 2025 12:29:47 +1000 Subject: [PATCH] refactor again --- .../components/ui/EditorPanel/EditorPanel.tsx | 121 +++++++++++++----- .../ui/EditorPanel/EditorPanel.types.ts | 27 ---- .../pages/project/[ref]/auth/policies.tsx | 88 ++++++++----- .../project/[ref]/database/functions.tsx | 62 +++++---- .../pages/project/[ref]/database/triggers.tsx | 53 ++++---- 5 files changed, 212 insertions(+), 139 deletions(-) delete mode 100644 apps/studio/components/ui/EditorPanel/EditorPanel.types.ts diff --git a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx index b041e4b261e..c763229f2c5 100644 --- a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx +++ b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx @@ -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) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() const [currentValue, setCurrentValue] = useState(initialValue) - const [savedCode, setSavedCode] = useState('') - 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 ( !open && onClose()}> - {label} +
+ SQL Editor + {label && {label}} +
{templates.length > 0 && ( @@ -371,6 +400,34 @@ export const EditorPanel = ({ )}
+ {onSave && ( + +
{ + onSave(values.saveValue) + })} + className="flex items-center gap-2" + > + {saveValue && ( + ( + + )} + /> + )} + + +
+ )} 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 -} \ No newline at end of file diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index cc72f25de5a..856176df28b 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -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() const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false) const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState() - + // Local editor panel state const [editorPanelOpen, setEditorPanelOpen] = useState(false) - const [editorPanelProps, setEditorPanelProps] = useState({}) 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" /> - + 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, + })) + : [] + } />
) diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index 9e3aaf24cb0..c4653c0b56b 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -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({}) + 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} /> - + 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...' + } /> ) diff --git a/apps/studio/pages/project/[ref]/database/triggers.tsx b/apps/studio/pages/project/[ref]/database/triggers.tsx index 7803982b879..587f409b39d 100644 --- a/apps/studio/pages/project/[ref]/database/triggers.tsx +++ b/apps/studio/pages/project/[ref]/database/triggers.tsx @@ -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() const [showCreateTriggerForm, setShowCreateTriggerForm] = useState(false) const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState(false) - + // Local editor panel state const [editorPanelOpen, setEditorPanelOpen] = useState(false) - const [editorPanelProps, setEditorPanelProps] = useState({}) + const [selectedTriggerForEditor, setSelectedTriggerForEditor] = useState() 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} /> - + 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...' + } /> )