mirror of
https://github.com/apache/superset.git
synced 2026-04-27 12:05:24 +00:00
feat: [SQLLAB] add checkbox to control autocomplete (#9338)
* [SQLLAB] add checkbox to control autocomplete * autocomplete -> autocompleteEnabled * fix defaultProps * fix spec
This commit is contained in:
272
superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx
Normal file
272
superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user