Extensions can replace Superset's default text editors with custom implementations. This allows you to provide enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions.
Superset uses text editors in various places throughout the application:
| Language | Locations |
|---|---|
sql | SQL Lab, Metric/Filter Popovers |
json | Dashboard Properties, Annotation Modal, Theme Modal |
css | Dashboard Properties, CSS Template Modal |
markdown | Dashboard Markdown component |
yaml | Template Params Editor |
javascript | Custom JavaScript editor contexts |
python | Custom Python editor contexts |
text | Plain text editor contexts |
By registering an editor for a language, your extension replaces the default Ace editor in all locations that use that language.
Your editor component must implement the EditorProps interface and expose an EditorHandle via forwardRef. For the complete interface definitions, see @apache-superset/core/api/editors.ts.
interface EditorProps { /** Controlled value */ value: string; /** Content change handler */ onChange: (value: string) => void; /** Language mode for syntax highlighting */ language: EditorLanguage; /** Keyboard shortcuts to register */ hotkeys?: EditorHotkey[]; /** Callback when editor is ready with imperative handle */ onReady?: (handle: EditorHandle) => void; /** Host-specific context (e.g., database info from SQL Lab) */ metadata?: Record<string, unknown>; // ... additional props for styling, annotations, etc. }
interface EditorHandle { /** Focus the editor */ focus(): void; /** Get the current editor content */ getValue(): string; /** Get the current cursor position */ getCursorPosition(): Position; /** Move the cursor to a specific position */ moveCursorToPosition(position: Position): void; /** Set the selection range */ setSelection(selection: Range): void; /** Scroll to a specific line */ scrollToLine(line: number): void; // ... additional methods for text manipulation, annotations, etc. }
Here's an example of a Monaco-based SQL editor implementing the key interfaces shown above:
import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react'; import * as monaco from 'monaco-editor'; import type { editors } from '@apache-superset/core'; const MonacoSQLEditor = forwardRef<editors.EditorHandle, editors.EditorProps>( (props, ref) => { const { value, onChange, hotkeys, onReady } = props; const containerRef = useRef<HTMLDivElement>(null); const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null); // Implement EditorHandle interface const handle: editors.EditorHandle = { focus: () => editorRef.current?.focus(), getValue: () => editorRef.current?.getValue() ?? '', getCursorPosition: () => { const pos = editorRef.current?.getPosition(); return { line: (pos?.lineNumber ?? 1) - 1, column: (pos?.column ?? 1) - 1 }; }, // ... implement remaining methods }; useImperativeHandle(ref, () => handle, []); useEffect(() => { if (!containerRef.current) return; const editor = monaco.editor.create(containerRef.current, { value, language: 'sql' }); editorRef.current = editor; editor.onDidChangeModelContent(() => onChange(editor.getValue())); // Register hotkeys hotkeys?.forEach(hotkey => { editor.addAction({ id: hotkey.name, label: hotkey.name, run: () => hotkey.exec(handle), }); }); onReady?.(handle); return () => editor.dispose(); }, []); return <div ref={containerRef} style={{ height: '100%', width: '100%' }} />; }, ); export default MonacoSQLEditor;
Register the editor at module load time from your extension's entry point:
import { editors } from '@apache-superset/core'; import MonacoSQLEditor from './MonacoSQLEditor'; editors.registerEditor( { id: 'my-extension.monaco-sql', name: 'Monaco SQL Editor', languages: ['sql'], }, MonacoSQLEditor, );
Superset passes keyboard shortcuts via the hotkeys prop. Each hotkey includes an exec function that receives the EditorHandle:
interface EditorHotkey { name: string; key: string; // e.g., "Ctrl-Enter", "Alt-Shift-F" description?: string; exec: (handle: EditorHandle) => void; }
Your editor must register these hotkeys with your editor framework and call exec(handle) when triggered.
Superset passes static autocomplete suggestions via the keywords prop. These include table names, column names, and SQL functions based on the current database context:
interface EditorKeyword { name: string; value?: string; // Text to insert (defaults to name) meta?: string; // Category like "table", "column", "function" score?: number; // Sorting priority }
Your editor should convert these to your framework's completion format and register them for autocomplete.
For dynamic autocomplete (e.g., fetching suggestions as the user types), implement and register a CompletionProvider via the EditorHandle:
const provider: CompletionProvider = { id: 'my-sql-completions', triggerCharacters: ['.', ' '], provideCompletions: async (content, position, context) => { // Use context.metadata for database info // Return array of CompletionItem return [ { label: 'SELECT', insertText: 'SELECT', kind: 'keyword' }, // ... ]; }, }; // Register during editor initialization const disposable = handle.registerCompletionProvider(provider);