import { debounce } from 'lodash' import { Book, Save, X } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { createSqlSnippetSkeletonV2, suffixWithLimit, } from 'components/interfaces/SQLEditor/SQLEditor.utils' import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { SqlRunButton } from 'components/interfaces/SQLEditor/UtilityPanel/RunButton' 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 { BASE_PATH } from 'lib/constants' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' import { useAppStateSnapshot } from 'state/app-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Button, cn, CodeBlock, Command_Shadcn_, CommandEmpty_Shadcn_, CommandGroup_Shadcn_, CommandInput_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, HoverCard_Shadcn_, HoverCardContent_Shadcn_, HoverCardTrigger_Shadcn_, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, SQL_ICON, } from 'ui' import { Admonition } from 'ui-patterns' import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils' import AIEditor from '../AIEditor' import { ButtonTooltip } from '../ButtonTooltip' import { InlineLink } from '../InlineLink' import SqlWarningAdmonition from '../SqlWarningAdmonition' interface EditorPanelProps { onChange?: (value: string) => void } export const EditorPanel = ({ onChange }: EditorPanelProps) => { const { ref } = useParams() const project = useSelectedProject() const { editorPanel, setEditorPanel } = useAppStateSnapshot() const { profile } = useProfile() const snapV2 = useSqlEditorV2StateSnapshot() const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation() const { includeSchemaMetadata } = useOrgAiOptInLevel() const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState() const [results, setResults] = useState(undefined) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() const [currentValue, setCurrentValue] = useState(editorPanel.initialValue || '') const [savedCode, setSavedCode] = useState('') const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false) const [showResults, setShowResults] = useState(true) const [isTemplatesOpen, setIsTemplatesOpen] = useState(false) const errorHeader = error?.formattedError?.split('\n')?.filter((x: string) => x.length > 0)?.[0] const errorContent = error?.formattedError ?.split('\n') ?.filter((x: string) => x.length > 0) ?.slice(1) ?? [] const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: async (res) => { setResults(res.result) }, onError: (error) => { setError(error) setResults([]) }, }) const onExecuteSql = (skipValidation = false) => { setError(undefined) setShowWarning(undefined) if (currentValue.length === 0) return if (!skipValidation) { const isReadOnlySelectSQL = isReadOnlySelect(currentValue) if (!isReadOnlySelectSQL) { const hasUnknownFunctions = containsUnknownFunction(currentValue) setShowWarning(hasUnknownFunctions ? 'hasUnknownFunctions' : 'hasWriteOperation') return } } if (editorPanel.onSave) { editorPanel.onSave(currentValue) } executeSql({ sql: suffixWithLimit(currentValue, 100), projectRef: project?.ref, connectionString: project?.connectionString, handleError: (error) => { throw error }, contextualInvalidation: true, }) } const handleChange = (value: string) => { setCurrentValue(value) onChange?.(value) } const onSelectTemplate = (content: string) => { handleChange(content) 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 (editorPanel.initialValue !== undefined && editorPanel.initialValue !== currentValue) { setCurrentValue(editorPanel.initialValue) } }, [editorPanel.initialValue]) useEffect(() => { if (editorPanel.initialValue !== currentValue) { setEditorPanel({ initialValue: currentValue, }) } }, [currentValue, setEditorPanel]) return (
SQL Editor
No templates found. {editorPanel.templates?.map((template) => ( onSelectTemplate(template.content)} className="cursor-pointer" >

{template.name}

{template.description}

))}
} onClick={async () => { if (!ref) return console.error('Project ref is required') if (!project) return console.error('Project is required') if (!profile) return console.error('Profile is required') try { setIsSaving(true) const { title: name } = await generateSqlTitle({ sql: currentValue, }) const snippet = createSqlSnippetSkeletonV2({ id: uuidv4(), name, sql: currentValue, owner_id: profile.id, project_id: project.id, }) snapV2.addSnippet({ projectRef: ref, snippet }) snapV2.addNeedsSaving(snippet.id) toast.success(
Saved snippet! View it{' '} here
) } catch (error: any) { toast.error(`Failed to create new query: ${error.message}`) } finally { setIsSaving(false) } }} />
{error !== undefined && (
{errorContent.length > 0 ? ( errorContent.map((errorText: string, i: number) => (
                        {errorText}
                      
)) ) : (

{error.error}

)}
} />
)} {showWarning && ( setShowWarning(undefined)} onConfirm={() => { setShowWarning(undefined) onExecuteSql(true) }} /> )} {results !== undefined && results.length > 0 && (
{showResults && (
)}

{results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`}

)} {results !== undefined && results.length === 0 && (

Success. No rows returned.

)}
) } export default EditorPanel