Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sqllab): Replace autocomplete logic by a hook #24677

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion superset-frontend/spec/helpers/testing-library.tsx
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ type Options = Omit<RenderOptions, 'queries'> & {
store?: Store;
};

const createStore = (initialState: object = {}, reducers: object = {}) =>
export const createStore = (initialState: object = {}, reducers: object = {}) =>
configureStore({
preloadedState: initialState,
reducer: {
6 changes: 3 additions & 3 deletions superset-frontend/src/SqlLab/App.jsx
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import { initFeatureFlags, isFeatureEnabled } from 'src/featureFlags';
import { setupStore } from 'src/views/store';
import setupExtensions from 'src/setup/setupExtensions';
import getBootstrapData from 'src/utils/getBootstrapData';
import { api } from 'src/hooks/apiResources/queryApi';
import { tableApiUtil } from 'src/hooks/apiResources/tables';
import getInitialState from './reducers/getInitialState';
import { reducers } from './reducers/index';
import App from './components/App';
@@ -127,14 +127,14 @@ initialState.sqlLab.tables.forEach(
({ name: table, schema, dbId, persistData }) => {
if (dbId && schema && table && persistData?.columns) {
store.dispatch(
api.util.upsertQueryData(
tableApiUtil.upsertQueryData(
'tableMetadata',
{ dbId, schema, table },
persistData,
),
);
store.dispatch(
api.util.upsertQueryData(
tableApiUtil.upsertQueryData(
'tableExtendedMetadata',
{ dbId, schema, table },
{},
174 changes: 16 additions & 158 deletions superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -16,38 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { css, styled, usePrevious, t } from '@superset-ui/core';
import React, { useState, useEffect, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { css, styled, usePrevious } from '@superset-ui/core';

import { areArraysShallowEqual } from 'src/reduxUtils';
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
import {
queryEditorSetSelectedText,
addTable,
addDangerToast,
} from 'src/SqlLab/actions/sqlLab';
import {
SCHEMA_AUTOCOMPLETE_SCORE,
TABLE_AUTOCOMPLETE_SCORE,
COLUMN_AUTOCOMPLETE_SCORE,
SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
} from 'src/SqlLab/constants';
import {
Editor,
AceCompleterKeyword,
FullSQLEditor as AceEditor,
} from 'src/components/AsyncAceEditor';
import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab';
import { FullSQLEditor as AceEditor } from 'src/components/AsyncAceEditor';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import {
useSchemas,
useTables,
tableEndpoints,
skipToken,
} from 'src/hooks/apiResources';
import { useDatabaseFunctionsQuery } from 'src/hooks/apiResources/databaseFunctions';
import { RootState } from 'src/views/store';
import { useAnnotations } from './useAnnotations';
import { useKeywords } from './useKeywords';

type HotKey = {
key: string;
@@ -101,68 +78,10 @@ const AceEditorWrapper = ({
'schema',
'templateParams',
]);
const { data: schemaOptions } = useSchemas({
...(autocomplete && { dbId: queryEditor.dbId }),
});
const { data: tableData } = useTables({
...(autocomplete && {
dbId: queryEditor.dbId,
schema: queryEditor.schema,
}),
});

const { data: functionNames, isError } = useDatabaseFunctionsQuery(
{ dbId: queryEditor.dbId },
{ skip: !autocomplete || !queryEditor.dbId },
);

useEffect(() => {
if (isError) {
dispatch(
addDangerToast(t('An error occurred while fetching function names.')),
);
}
}, [dispatch, isError]);

const currentSql = queryEditor.sql ?? '';

// Loading schema, table and column names as auto-completable words
const { schemas, schemaWords } = useMemo(
() => ({
schemas: schemaOptions ?? [],
schemaWords: (schemaOptions ?? []).map(s => ({
name: s.label,
value: s.value,
score: SCHEMA_AUTOCOMPLETE_SCORE,
meta: 'schema',
})),
}),
[schemaOptions],
);
const tables = tableData?.options ?? [];

const columns = useSelector<RootState, string[]>(state => {
const columns = new Set<string>();
tables.forEach(({ value }) => {
tableEndpoints.tableMetadata
.select(
queryEditor.dbId && queryEditor.schema
? {
dbId: queryEditor.dbId,
schema: queryEditor.schema,
table: value,
}
: skipToken,
)(state)
.data?.columns?.forEach(({ name }) => {
columns.add(name);
});
});
return [...columns];
}, shallowEqual);

const [sql, setSql] = useState(currentSql);
const [words, setWords] = useState<AceCompleterKeyword[]>([]);

// The editor changeSelection is called multiple times in a row,
// faster than React reconciliation process, so the selected text
@@ -173,24 +92,10 @@ const AceEditorWrapper = ({
useEffect(() => {
// Making sure no text is selected from previous mount
dispatch(queryEditorSetSelectedText(queryEditor, null));
setAutoCompleter();
}, []);

const prevTables = usePrevious(tables) ?? [];
const prevSchemas = usePrevious(schemas) ?? [];
const prevColumns = usePrevious(columns) ?? [];
const prevSql = usePrevious(currentSql);

useEffect(() => {
if (
!areArraysShallowEqual(tables, prevTables) ||
!areArraysShallowEqual(schemas, prevSchemas) ||
!areArraysShallowEqual(columns, prevColumns)
) {
setAutoCompleter();
}
}, [tables, schemas, columns]);

useEffect(() => {
if (currentSql !== prevSql) {
setSql(currentSql);
@@ -243,72 +148,25 @@ const AceEditorWrapper = ({
onChange(text);
};

function setAutoCompleter() {
const tableWords = tables.map(t => {
const tableName = t.value;

return {
name: t.label,
value: tableName,
score: TABLE_AUTOCOMPLETE_SCORE,
meta: 'table',
};
});

const columnWords = columns.map(col => ({
name: col,
value: col,
score: COLUMN_AUTOCOMPLETE_SCORE,
meta: 'column',
}));

const functionWords = (functionNames ?? []).map(func => ({
name: func,
value: func,
score: SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
meta: 'function',
}));

const completer = {
insertMatch: (editor: Editor, data: any) => {
if (data.meta === 'table') {
dispatch(addTable(queryEditor, data.value, queryEditor.schema));
}

let { caption } = data;
if (data.meta === 'table' && caption.includes(' ')) {
caption = `"${caption}"`;
}

// executing https://github.com/thlorenz/brace/blob/3a00c5d59777f9d826841178e1eb36694177f5e6/ext/language_tools.js#L1448
editor.completer.insertMatch(
`${caption}${['function', 'schema'].includes(data.meta) ? '' : ' '}`,
);
},
};

const words = schemaWords
.concat(tableWords)
.concat(columnWords)
.concat(functionWords)
.concat(sqlKeywords)
.map(word => ({
...word,
completer,
}));

setWords(words);
}
const { data: annotations } = useAnnotations({
dbId: queryEditor.dbId,
schema: queryEditor.schema,
sql: currentSql,
templateParams: queryEditor.templateParams,
});

const keywords = useKeywords(
{
queryEditorId,
dbId: queryEditor.dbId,
schema: queryEditor.schema,
},
!autocomplete,
);

return (
<StyledAceEditor
keywords={words}
keywords={keywords}
onLoad={onEditorLoad}
onBlur={onBlurSql}
height={height}
Loading