mirror of
https://github.com/apache/superset.git
synced 2026-04-26 19:44:58 +00:00
* [SQLLAB] add checkbox to control autocomplete * autocomplete -> autocompleteEnabled * fix defaultProps * fix spec
273 lines
7.6 KiB
TypeScript
273 lines
7.6 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
import React from 'react';
|
|
import AceEditor from 'react-ace';
|
|
import 'brace/mode/sql';
|
|
import 'brace/theme/github';
|
|
import 'brace/ext/language_tools';
|
|
import ace from 'brace';
|
|
import { areArraysShallowEqual } from '../../reduxUtils';
|
|
import sqlKeywords from '../utils/sqlKeywords';
|
|
import {
|
|
SCHEMA_AUTOCOMPLETE_SCORE,
|
|
TABLE_AUTOCOMPLETE_SCORE,
|
|
COLUMN_AUTOCOMPLETE_SCORE,
|
|
SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
|
|
} from '../constants';
|
|
|
|
const langTools = ace.acequire('ace/ext/language_tools');
|
|
|
|
type HotKey = {
|
|
key: string;
|
|
descr: string;
|
|
name: string;
|
|
func: () => void;
|
|
};
|
|
|
|
interface Props {
|
|
actions: {
|
|
queryEditorSetSelectedText: (edit: any, text: null | string) => void;
|
|
addTable: (queryEditor: any, value: any, schema: any) => void;
|
|
};
|
|
autocomplete: boolean;
|
|
onBlur: (sql: string) => void;
|
|
sql: string;
|
|
schemas: any[];
|
|
tables: any[];
|
|
functionNames: string[];
|
|
extendedTables: Array<{ name: string; columns: any[] }>;
|
|
queryEditor: any;
|
|
height: string;
|
|
hotkeys: HotKey[];
|
|
onChange: (sql: string) => void;
|
|
}
|
|
|
|
interface State {
|
|
sql: string;
|
|
selectedText: string;
|
|
words: any[];
|
|
}
|
|
|
|
class AceEditorWrapper extends React.PureComponent<Props, State> {
|
|
static defaultProps = {
|
|
onBlur: () => {},
|
|
onChange: () => {},
|
|
schemas: [],
|
|
tables: [],
|
|
functionNames: [],
|
|
extendedTables: [],
|
|
};
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
sql: props.sql,
|
|
selectedText: '',
|
|
words: [],
|
|
};
|
|
this.onChange = this.onChange.bind(this);
|
|
}
|
|
componentDidMount() {
|
|
// Making sure no text is selected from previous mount
|
|
this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null);
|
|
this.setAutoCompleter(this.props);
|
|
}
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
if (
|
|
!areArraysShallowEqual(this.props.tables, nextProps.tables) ||
|
|
!areArraysShallowEqual(this.props.schemas, nextProps.schemas) ||
|
|
!areArraysShallowEqual(
|
|
this.props.extendedTables,
|
|
nextProps.extendedTables,
|
|
)
|
|
) {
|
|
this.setAutoCompleter(nextProps);
|
|
}
|
|
if (nextProps.sql !== this.props.sql) {
|
|
this.setState({ sql: nextProps.sql });
|
|
}
|
|
}
|
|
onBlur() {
|
|
this.props.onBlur(this.state.sql);
|
|
}
|
|
onAltEnter() {
|
|
this.props.onBlur(this.state.sql);
|
|
}
|
|
onEditorLoad(editor: any) {
|
|
editor.commands.addCommand({
|
|
name: 'runQuery',
|
|
bindKey: { win: 'Alt-enter', mac: 'Alt-enter' },
|
|
exec: () => {
|
|
this.onAltEnter();
|
|
},
|
|
});
|
|
this.props.hotkeys.forEach(keyConfig => {
|
|
editor.commands.addCommand({
|
|
name: keyConfig.name,
|
|
bindKey: { win: keyConfig.key, mac: keyConfig.key },
|
|
exec: keyConfig.func,
|
|
});
|
|
});
|
|
editor.$blockScrolling = Infinity; // eslint-disable-line no-param-reassign
|
|
editor.selection.on('changeSelection', () => {
|
|
const selectedText = editor.getSelectedText();
|
|
// Backspace trigger 1 character selection, ignoring
|
|
if (
|
|
selectedText !== this.state.selectedText &&
|
|
selectedText.length !== 1
|
|
) {
|
|
this.setState({ selectedText });
|
|
this.props.actions.queryEditorSetSelectedText(
|
|
this.props.queryEditor,
|
|
selectedText,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
onChange(text: string) {
|
|
this.setState({ sql: text });
|
|
this.props.onChange(text);
|
|
}
|
|
getCompletions(
|
|
aceEditor: any,
|
|
session: any,
|
|
pos: any,
|
|
prefix: string,
|
|
callback: (p0: any, p1: any[]) => void,
|
|
) {
|
|
// If the prefix starts with a number, don't try to autocomplete with a
|
|
// table name or schema or anything else
|
|
if (!isNaN(parseInt(prefix, 10))) {
|
|
return;
|
|
}
|
|
const completer = {
|
|
insertMatch: (editor: any, data: any) => {
|
|
if (data.meta === 'table') {
|
|
this.props.actions.addTable(
|
|
this.props.queryEditor,
|
|
data.value,
|
|
this.props.queryEditor.schema,
|
|
);
|
|
}
|
|
editor.completer.insertMatch({
|
|
value: `${data.caption}${
|
|
['function', 'schema'].includes(data.meta) ? '' : ' '
|
|
}`,
|
|
});
|
|
},
|
|
};
|
|
// Mutate instead of object spread here for performance
|
|
const words = this.state.words.map(word => {
|
|
/* eslint-disable-next-line no-param-reassign */
|
|
word.completer = completer;
|
|
return word;
|
|
});
|
|
callback(null, words);
|
|
}
|
|
setAutoCompleter(props: Props) {
|
|
// Loading schema, table and column names as auto-completable words
|
|
const schemas = props.schemas || [];
|
|
const schemaWords = schemas.map(s => ({
|
|
name: s.label,
|
|
value: s.value,
|
|
score: SCHEMA_AUTOCOMPLETE_SCORE,
|
|
meta: 'schema',
|
|
}));
|
|
const columns = {};
|
|
const tables = props.tables || [];
|
|
const extendedTables = props.extendedTables || [];
|
|
const tableWords = tables.map(t => {
|
|
const tableName = t.value;
|
|
const extendedTable = extendedTables.find(et => et.name === tableName);
|
|
const cols = (extendedTable && extendedTable.columns) || [];
|
|
cols.forEach(col => {
|
|
columns[col.name] = null; // using an object as a unique set
|
|
});
|
|
return {
|
|
name: t.label,
|
|
value: tableName,
|
|
score: TABLE_AUTOCOMPLETE_SCORE,
|
|
meta: 'table',
|
|
};
|
|
});
|
|
|
|
const columnWords = Object.keys(columns).map(col => ({
|
|
name: col,
|
|
value: col,
|
|
score: COLUMN_AUTOCOMPLETE_SCORE,
|
|
meta: 'column',
|
|
}));
|
|
|
|
const functionWords = props.functionNames.map(func => ({
|
|
name: func,
|
|
value: func,
|
|
score: SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
|
|
meta: 'function',
|
|
}));
|
|
|
|
const words = schemaWords
|
|
.concat(tableWords)
|
|
.concat(columnWords)
|
|
.concat(functionWords)
|
|
.concat(sqlKeywords);
|
|
|
|
this.setState({ words }, () => {
|
|
const completer = {
|
|
getCompletions: this.getCompletions.bind(this),
|
|
};
|
|
if (langTools) {
|
|
langTools.setCompleters([completer]);
|
|
}
|
|
});
|
|
}
|
|
getAceAnnotations() {
|
|
const validationResult = this.props.queryEditor.validationResult;
|
|
const resultIsReady = validationResult && validationResult.completed;
|
|
if (resultIsReady && validationResult.errors.length > 0) {
|
|
const errors = validationResult.errors.map((err: any) => ({
|
|
type: 'error',
|
|
row: err.line_number - 1,
|
|
column: err.start_column - 1,
|
|
text: err.message,
|
|
}));
|
|
return errors;
|
|
}
|
|
return [];
|
|
}
|
|
render() {
|
|
return (
|
|
<AceEditor
|
|
mode="sql"
|
|
theme="github"
|
|
onLoad={this.onEditorLoad.bind(this)}
|
|
onBlur={this.onBlur.bind(this)}
|
|
height={this.props.height}
|
|
onChange={this.onChange}
|
|
width="100%"
|
|
editorProps={{ $blockScrolling: true }}
|
|
enableLiveAutocompletion={this.props.autocomplete}
|
|
value={this.state.sql}
|
|
annotations={this.getAceAnnotations()}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default AceEditorWrapper;
|