mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 12:55:06 +03:00
* Use the .sql suffix when generating ids. * Fix a bug where a new snippet would not show up in the snippet list until refresh. * Add API routes which serve file snippets. * Refactor the renameSnippet and moveSnippet to work with file snippets. * Change the link to the SQL Editor. * Minor fixes from CodeRabbit. * Check the file/folder name for invalid chars. * More fixes from CodeRabbit review. * Fix minor issues. * Use zod to parse the snippet ids when deleting. * Try to fix snyk issue. * Add validation to the GET content index route. * Minor fixes. * Show create a new folder, it was hidden by mistake. * Add SNIPPETS_MANAGEMENT_FOLDER env var. * Add snippets folder in the docker-compose. * Add error toasts if the env var is not set. * Add snippets management folder to the generateLocalEnv script. * Revert the docker-compose changes, will be done in a followup PR. * Revert also the snippets volume folder. * Remove unneeded line.
290 lines
9.7 KiB
TypeScript
290 lines
9.7 KiB
TypeScript
import Editor, { Monaco, OnMount } from '@monaco-editor/react'
|
|
import { useRouter } from 'next/router'
|
|
import { MutableRefObject, useEffect, useRef, useState } from 'react'
|
|
|
|
import { useDebounce } from '@uidotdev/usehooks'
|
|
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
|
|
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
|
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
|
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
|
import { useProfile } from 'lib/profile'
|
|
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
|
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
|
|
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
|
import { useTabsStateSnapshot } from 'state/tabs'
|
|
import { cn } from 'ui'
|
|
import { Admonition } from 'ui-patterns'
|
|
import type { IStandaloneCodeEditor } from './SQLEditor.types'
|
|
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
|
|
|
|
export type MonacoEditorProps = {
|
|
id: string
|
|
snippetName: string
|
|
className?: string
|
|
editorRef: MutableRefObject<IStandaloneCodeEditor | null>
|
|
monacoRef: MutableRefObject<Monaco | null>
|
|
autoFocus?: boolean
|
|
executeQuery: () => void
|
|
onHasSelection: (value: boolean) => void
|
|
onMount?: (editor: IStandaloneCodeEditor) => void
|
|
onPrompt?: (value: {
|
|
selection: string
|
|
beforeSelection: string
|
|
afterSelection: string
|
|
startLineNumber: number
|
|
endLineNumber: number
|
|
}) => void
|
|
placeholder?: string
|
|
}
|
|
|
|
const MonacoEditor = ({
|
|
id,
|
|
snippetName,
|
|
editorRef,
|
|
monacoRef,
|
|
autoFocus = true,
|
|
placeholder = '',
|
|
className,
|
|
executeQuery,
|
|
onHasSelection,
|
|
onPrompt,
|
|
onMount,
|
|
}: MonacoEditorProps) => {
|
|
const router = useRouter()
|
|
const { profile } = useProfile()
|
|
const { ref, content } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const snapV2 = useSqlEditorV2StateSnapshot()
|
|
const tabsSnap = useTabsStateSnapshot()
|
|
const aiSnap = useAiAssistantStateSnapshot()
|
|
const { openSidebar, toggleSidebar } = useSidebarManagerSnapshot()
|
|
|
|
const [intellisenseEnabled] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
|
|
true
|
|
)
|
|
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
|
|
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
|
|
true
|
|
)
|
|
|
|
// [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
|
|
const [value, setValue] = useState('')
|
|
const debouncedValue = useDebounce(value, 1000)
|
|
|
|
const snippet = snapV2.snippets[id]
|
|
const disableEdit =
|
|
snippet?.snippet.visibility === 'project' && snippet?.snippet.owner_id !== profile?.id
|
|
|
|
const executeQueryRef = useRef(executeQuery)
|
|
executeQueryRef.current = executeQuery
|
|
|
|
const handleEditorOnMount: OnMount = async (editor, monaco) => {
|
|
editorRef.current = editor
|
|
monacoRef.current = monaco
|
|
|
|
const model = editorRef.current.getModel()
|
|
if (model !== null) {
|
|
monacoRef.current.editor.setModelMarkers(model, 'owner', [])
|
|
}
|
|
|
|
editor.addAction({
|
|
id: 'run-query',
|
|
label: 'Run Query',
|
|
keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],
|
|
contextMenuGroupId: 'operation',
|
|
contextMenuOrder: 0,
|
|
run: () => {
|
|
executeQueryRef.current()
|
|
},
|
|
})
|
|
|
|
editor.addAction({
|
|
id: 'save-query',
|
|
label: 'Save Query',
|
|
keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyS],
|
|
contextMenuGroupId: 'operation',
|
|
contextMenuOrder: 0,
|
|
run: () => {
|
|
if (snippet) snapV2.addNeedsSaving(snippet.snippet.id)
|
|
},
|
|
})
|
|
|
|
editor.addAction({
|
|
id: 'explain-code',
|
|
label: 'Explain Code',
|
|
contextMenuGroupId: 'operation',
|
|
contextMenuOrder: 1,
|
|
run: () => {
|
|
const selectedValue = (editorRef?.current as any)
|
|
.getModel()
|
|
.getValueInRange((editorRef?.current as any)?.getSelection())
|
|
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
|
|
aiSnap.newChat({
|
|
name: 'Explain code section',
|
|
sqlSnippets: [selectedValue],
|
|
initialInput: 'Can you explain this section to me in more detail?',
|
|
})
|
|
},
|
|
})
|
|
|
|
editor.addAction({
|
|
id: 'toggle-ai-assistant',
|
|
label: 'Toggle AI Assistant',
|
|
keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI],
|
|
run: () => {
|
|
if (isAIAssistantHotkeyEnabled) {
|
|
toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
|
|
}
|
|
},
|
|
})
|
|
|
|
if (onPrompt) {
|
|
editor.addAction({
|
|
id: 'generate-sql',
|
|
label: 'Generate SQL',
|
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK],
|
|
run: () => {
|
|
const selection = editor.getSelection()
|
|
const model = editor.getModel()
|
|
if (!model || !selection) return
|
|
|
|
const allLines = model.getLinesContent()
|
|
|
|
const startLineIndex = selection.startLineNumber - 1
|
|
const endLineIndex = selection.endLineNumber
|
|
|
|
const beforeSelection = allLines.slice(0, startLineIndex).join('\n') + '\n'
|
|
const selectedText = allLines.slice(startLineIndex, endLineIndex).join('\n')
|
|
const afterSelection = '\n' + allLines.slice(endLineIndex).join('\n')
|
|
|
|
onPrompt({
|
|
selection: selectedText,
|
|
beforeSelection,
|
|
afterSelection,
|
|
startLineNumber: selection?.startLineNumber ?? 0,
|
|
endLineNumber: selection?.endLineNumber ?? 0,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
editor.onDidChangeCursorSelection(({ selection }) => {
|
|
const noSelection =
|
|
selection.startLineNumber === selection.endLineNumber &&
|
|
selection.startColumn === selection.endColumn
|
|
onHasSelection(!noSelection)
|
|
})
|
|
|
|
if (autoFocus) {
|
|
if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 })
|
|
editor.focus()
|
|
}
|
|
|
|
onMount?.(editor)
|
|
}
|
|
|
|
function handleEditorChange(value: string | undefined) {
|
|
tabsSnap.makeActiveTabPermanent()
|
|
if (id && value) {
|
|
if (!snippet && ref && profile !== undefined && project !== undefined) {
|
|
const snippet = createSqlSnippetSkeletonV2({
|
|
idOverride: id,
|
|
name: snippetName,
|
|
sql: value,
|
|
owner_id: profile?.id,
|
|
project_id: project?.id,
|
|
})
|
|
snapV2.addSnippet({ projectRef: ref, snippet })
|
|
router.push(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
|
|
}
|
|
setValue(value)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (debouncedValue.length > 0 && snippet) {
|
|
const shouldInvalidate = snippet.snippet.isNotSavedInDatabaseYet
|
|
snapV2.setSql({ id, sql: value, shouldInvalidate })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [debouncedValue])
|
|
|
|
// if an SQL query is passed by the content parameter, set the editor value to its content. This
|
|
// is usually used for sending the user to SQL editor from other pages with SQL.
|
|
useEffect(() => {
|
|
if (content && content.length > 0) handleEditorChange(content)
|
|
}, [])
|
|
|
|
return (
|
|
<>
|
|
{disableEdit && (
|
|
<Admonition
|
|
type="default"
|
|
className="rounded-none border-0 border-b"
|
|
title="Read-only snippet"
|
|
description="This snippet has been shared to the project and is only editable by the owner who created this snippet. You may duplicate this snippet into a personal copy by right clicking on the snippet and selecting “Duplicate query”."
|
|
/>
|
|
)}
|
|
<Editor
|
|
className={cn(className, 'monaco-editor')}
|
|
theme={'supabase'}
|
|
onMount={handleEditorOnMount}
|
|
onChange={handleEditorChange}
|
|
defaultLanguage="pgsql"
|
|
defaultValue={snippet?.snippet.content?.sql}
|
|
path={id}
|
|
options={{
|
|
tabSize: 2,
|
|
fontSize: 13,
|
|
placeholder,
|
|
lineDecorationsWidth: 0,
|
|
readOnly: disableEdit,
|
|
minimap: { enabled: false },
|
|
wordWrap: 'on',
|
|
padding: { top: 4 },
|
|
// [Joshen] Commenting the following out as it causes the autocomplete suggestion popover
|
|
// to be positioned wrongly somehow. I'm not sure if this affects anything though, but leaving
|
|
// comment just in case anyone might be wondering. Relevant issues:
|
|
// - https://github.com/microsoft/monaco-editor/issues/2229
|
|
// - https://github.com/microsoft/monaco-editor/issues/2503
|
|
// fixedOverflowWidgets: true,
|
|
suggest: {
|
|
showMethods: intellisenseEnabled,
|
|
showFunctions: intellisenseEnabled,
|
|
showConstructors: intellisenseEnabled,
|
|
showDeprecated: intellisenseEnabled,
|
|
showFields: intellisenseEnabled,
|
|
showVariables: intellisenseEnabled,
|
|
showClasses: intellisenseEnabled,
|
|
showStructs: intellisenseEnabled,
|
|
showInterfaces: intellisenseEnabled,
|
|
showModules: intellisenseEnabled,
|
|
showProperties: intellisenseEnabled,
|
|
showEvents: intellisenseEnabled,
|
|
showOperators: intellisenseEnabled,
|
|
showUnits: intellisenseEnabled,
|
|
showValues: intellisenseEnabled,
|
|
showConstants: intellisenseEnabled,
|
|
showEnums: intellisenseEnabled,
|
|
showEnumMembers: intellisenseEnabled,
|
|
showKeywords: intellisenseEnabled,
|
|
showWords: intellisenseEnabled,
|
|
showColors: intellisenseEnabled,
|
|
showFiles: intellisenseEnabled,
|
|
showReferences: intellisenseEnabled,
|
|
showFolders: intellisenseEnabled,
|
|
showTypeParameters: intellisenseEnabled,
|
|
showIssues: intellisenseEnabled,
|
|
showUsers: intellisenseEnabled,
|
|
showSnippets: intellisenseEnabled,
|
|
},
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default MonacoEditor
|