feat: move ace-editor and mathjs to async modules (#10837)
Follow up on #10831, move brace and mathjs to async modules so that the initial page load for dashboards most pages can be faster.
This commit is contained in:
parent
5d529fd844
commit
0129c4253d
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
|
||||
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
|
||||
|
||||
export const CHECK_DASHBOARD_FAVORITE_ENDPOINT =
|
||||
'/superset/favstar/Dashboard/*/count';
|
||||
|
||||
/**
|
||||
* Drag an element and drop it to another element.
|
||||
* Usage:
|
||||
* drag(source).to(target);
|
||||
*/
|
||||
export function drag(selector: string, content: string | number | RegExp) {
|
||||
const dataTransfer = { data: {} };
|
||||
return {
|
||||
to(target: string | Cypress.Chainable) {
|
||||
cy.get('.dragdroppable')
|
||||
.contains(selector, content)
|
||||
.trigger('mousedown', { which: 1 })
|
||||
.trigger('dragstart', { dataTransfer })
|
||||
.trigger('drag', {});
|
||||
|
||||
(typeof target === 'string' ? cy.get(target) : target)
|
||||
.trigger('dragover', { dataTransfer })
|
||||
.trigger('drop', { dataTransfer })
|
||||
.trigger('dragend', { dataTransfer })
|
||||
.trigger('mouseup', { which: 1 });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
||||
import { WORLD_HEALTH_DASHBOARD, drag } from './dashboard.helper';
|
||||
|
||||
describe('Dashboard edit mode', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -45,19 +45,9 @@ describe('Dashboard edit mode', () => {
|
|||
.find('.chart-card-container')
|
||||
.contains('Box plot');
|
||||
|
||||
// drag-n-drop
|
||||
const dataTransfer = { data: {} };
|
||||
cy.get('.dragdroppable')
|
||||
.contains('Box plot')
|
||||
.trigger('mousedown', { which: 1 })
|
||||
.trigger('dragstart', { dataTransfer })
|
||||
.trigger('drag', {});
|
||||
cy.get('.grid-content div.grid-row.background--transparent')
|
||||
.last()
|
||||
.trigger('dragover', { dataTransfer })
|
||||
.trigger('drop', { dataTransfer })
|
||||
.trigger('dragend', { dataTransfer })
|
||||
.trigger('mouseup', { which: 1 });
|
||||
drag('.chart-card', 'Box plot').to(
|
||||
'.grid-row.background--transparent:last',
|
||||
);
|
||||
|
||||
// add back to dashboard
|
||||
cy.get('.grid-container .box_plot').should('be.exist');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* 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 { TABBED_DASHBOARD, drag } from './dashboard.helper';
|
||||
|
||||
describe('Dashboard edit markdown', () => {
|
||||
beforeEach(() => {
|
||||
cy.server();
|
||||
cy.login();
|
||||
cy.visit(TABBED_DASHBOARD);
|
||||
});
|
||||
|
||||
it('should load AceEditor on demand', () => {
|
||||
let numScripts = 0;
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
cy.get('.dashboard-header [data-test=pencil]').click();
|
||||
cy.get('script').then(nodes => {
|
||||
// load 5 new script chunks for css editor
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
// add new markdown component
|
||||
drag('.new-component', 'Markdown').to(
|
||||
'.grid-row.background--transparent:first',
|
||||
);
|
||||
cy.get('script').then(nodes => {
|
||||
// load more scripts for markdown editor
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
cy.contains('h3', '✨Markdown').click();
|
||||
cy.get('.ace_content').contains(
|
||||
'Click here to edit [markdown](https://bit.ly/1dQOfRK)',
|
||||
);
|
||||
|
||||
// entering edit mode does not add new scripts
|
||||
// (though scripts may still be removed by others)
|
||||
cy.get('script').then(nodes => {
|
||||
expect(nodes.length).to.most(numScripts);
|
||||
});
|
||||
|
||||
cy.get('.grid-row.background--transparent:first').click('right');
|
||||
cy.get('.ace_content').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
@ -25,14 +25,31 @@ describe('AdhocFilters', () => {
|
|||
cy.route('GET', '/superset/filter/table/*/name').as('filterValues');
|
||||
});
|
||||
|
||||
it('Set simple adhoc filter', () => {
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
it('Should not load mathjs when not needed', () => {
|
||||
cy.visitChartByName('Boys'); // a table chart
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 0);
|
||||
});
|
||||
|
||||
let numScripts = 0;
|
||||
|
||||
it('Should load AceEditor scripts when needed', () => {
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('.Select__control').scrollIntoView().click();
|
||||
cy.get('input[type=text]').focus().type('name{enter}');
|
||||
});
|
||||
|
||||
cy.get('script').then(nodes => {
|
||||
// should load new script chunks for SQL editor
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
});
|
||||
});
|
||||
|
||||
it('Set simple adhoc filter', () => {
|
||||
cy.get('#filter-edit-popover').within(() => {
|
||||
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
|
||||
cy.get('.Select__control').click();
|
||||
|
|
@ -40,7 +57,6 @@ describe('AdhocFilters', () => {
|
|||
});
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
|
|
@ -52,19 +68,21 @@ describe('AdhocFilters', () => {
|
|||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').focus().type('name{enter}');
|
||||
});
|
||||
cy.get('[data-test=adhoc_filters] .Select__control')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
cy.get('[data-test=adhoc_filters] input[type=text]')
|
||||
.focus()
|
||||
.type('name{enter}');
|
||||
|
||||
cy.wait('@filterValues');
|
||||
|
||||
cy.get('#filter-edit-popover').within(() => {
|
||||
cy.get('#adhoc-filter-edit-tabs-tab-SQL').click();
|
||||
cy.get('.ace_content').click();
|
||||
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'");
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
cy.get('#filter-edit-popover #adhoc-filter-edit-tabs-tab-SQL').click();
|
||||
cy.get('#filter-edit-popover .ace_content').click();
|
||||
cy.get('#filter-edit-popover .ace_text-input').type(
|
||||
"'Amy' OR name = 'Bob'",
|
||||
);
|
||||
cy.get('#filter-edit-popover button').contains('Save').click();
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({
|
||||
|
|
|
|||
|
|
@ -32,12 +32,25 @@ describe('Datasource control', () => {
|
|||
});
|
||||
|
||||
it('should allow edit datasource', () => {
|
||||
let numScripts = 0;
|
||||
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
cy.get('#datasource_menu').click();
|
||||
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
cy.get('a').contains('Edit Datasource').click();
|
||||
|
||||
// should load additional scripts for the modal
|
||||
cy.get('script').then(nodes => {
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
});
|
||||
|
||||
// create new metric
|
||||
cy.get('button').contains('Add Item').click();
|
||||
cy.get('table button').contains('Add Item', { timeout: 10000 }).click();
|
||||
cy.get('input[value="<new metric>"]').click();
|
||||
cy.get('input[value="<new metric>"]')
|
||||
.focus()
|
||||
|
|
@ -65,19 +78,33 @@ describe('Datasource control', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Groupby control', () => {
|
||||
it('Set groupby', () => {
|
||||
cy.server();
|
||||
describe('VizType control', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.server();
|
||||
cy.route('GET', '/superset/explore_json/**').as('getJson');
|
||||
cy.route('POST', '/superset/explore_json/**').as('postJson');
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
});
|
||||
|
||||
it('Can change vizType', () => {
|
||||
cy.visitChartByName('Daily Totals');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=groupby]').within(() => {
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('state{enter}');
|
||||
let numScripts = 0;
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
cy.get('.Control .label').contains('Table').click();
|
||||
|
||||
cy.get('[role="button"]').contains('Line Chart').click();
|
||||
|
||||
// should load mathjs for line chart
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||
cy.get('script').then(nodes => {
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
|
||||
});
|
||||
|
|
@ -118,3 +145,21 @@ describe('Time range filter', () => {
|
|||
cy.get('#filter-popover').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Groupby control', () => {
|
||||
it('Set groupby', () => {
|
||||
cy.server();
|
||||
cy.login();
|
||||
cy.route('GET', '/superset/explore_json/**').as('getJson');
|
||||
cy.route('POST', '/superset/explore_json/**').as('postJson');
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=groupby]').within(() => {
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').type('state{enter}');
|
||||
});
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ describe('Visualization > Line', () => {
|
|||
cy.get('.alert-warning').contains(`"Metrics" cannot be empty`);
|
||||
});
|
||||
|
||||
it('should preload mathjs', () => {
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||
cy.contains('Add Annotation Layer').scrollIntoView().click();
|
||||
// should not load additional mathjs
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||
cy.contains('Layer Configuration');
|
||||
});
|
||||
|
||||
it('should not show validator error when metric added', () => {
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
|
|
@ -68,6 +76,7 @@ describe('Visualization > Line', () => {
|
|||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should work with groupby', () => {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import { Provider } from 'react-redux';
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import AceEditor from 'react-ace';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import { MarkdownEditor } from 'src/components/AsyncAceEditor';
|
||||
import Markdown from 'src/dashboard/components/gridComponents/Markdown';
|
||||
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
|
|
@ -105,23 +105,23 @@ describe('Markdown', () => {
|
|||
|
||||
it('should render an Markdown when NOT focused', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(AceEditor)).not.toExist();
|
||||
expect(wrapper.find(MarkdownEditor)).not.toExist();
|
||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
||||
});
|
||||
|
||||
it('should render an AceEditor when focused and editMode=true and editorMode=edit', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(AceEditor)).not.toExist();
|
||||
expect(wrapper.find(MarkdownEditor)).not.toExist();
|
||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||
expect(wrapper.find(AceEditor)).toExist();
|
||||
expect(wrapper.find(MarkdownEditor)).toExist();
|
||||
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
||||
});
|
||||
|
||||
it('should render a ReactMarkdown when focused and editMode=true and editorMode=preview', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||
expect(wrapper.find(AceEditor)).toExist();
|
||||
expect(wrapper.find(MarkdownEditor)).toExist();
|
||||
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
||||
|
||||
// we can't call setState on Markdown bc it's not the root component, so call
|
||||
|
|
@ -131,7 +131,7 @@ describe('Markdown', () => {
|
|||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
||||
expect(wrapper.find(AceEditor)).not.toExist();
|
||||
expect(wrapper.find(MarkdownEditor)).not.toExist();
|
||||
});
|
||||
|
||||
it('should call updateComponents when editMode changes from edit => preview, and there are markdownSource changes', () => {
|
||||
|
|
@ -148,7 +148,7 @@ describe('Markdown', () => {
|
|||
dropdown.prop('onChange')('edit');
|
||||
// because we can't call setState on Markdown, change it through the editor
|
||||
// then go back to preview mode to invoke updateComponents
|
||||
const editor = wrapper.find(AceEditor);
|
||||
const editor = wrapper.find(MarkdownEditor);
|
||||
editor.prop('onChange')('new markdown!');
|
||||
dropdown.prop('onChange')('preview');
|
||||
expect(updateComponents.callCount).toBe(1);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||
import { FormControl } from 'react-bootstrap';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import AceEditor from 'react-ace';
|
||||
import { TextAreaEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||
|
||||
|
|
@ -52,6 +52,6 @@ describe('SelectControl', () => {
|
|||
props.language = 'markdown';
|
||||
wrapper = shallow(<TextAreaControl {...props} />);
|
||||
expect(wrapper.find(FormControl)).not.toExist();
|
||||
expect(wrapper.find(AceEditor)).toExist();
|
||||
expect(wrapper.find(TextAreaEditor)).toExist();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -175,10 +175,10 @@ export default class CRUDCollection extends React.PureComponent<
|
|||
))}
|
||||
{extraButtons}
|
||||
{allowDeletes && !allowAddItem && (
|
||||
<th aria-label="Delete" className="tiny-cell" />
|
||||
<th key="delete-item" aria-label="Delete" className="tiny-cell" />
|
||||
)}
|
||||
{allowAddItem && (
|
||||
<th>
|
||||
<th key="add-item">
|
||||
<Button buttonStyle="primary" onClick={this.onAddItem}>
|
||||
<i className="fa fa-plus" /> {t('Add Item')}
|
||||
</Button>
|
||||
|
|
@ -237,7 +237,7 @@ export default class CRUDCollection extends React.PureComponent<
|
|||
)),
|
||||
);
|
||||
if (allowAddItem) {
|
||||
tds.push(<td />);
|
||||
tds.push(<td key="add" />);
|
||||
}
|
||||
if (allowDeletes) {
|
||||
tds.push(
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@
|
|||
* 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 'src/reduxUtils';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
import {
|
||||
|
|
@ -30,8 +25,11 @@ import {
|
|||
COLUMN_AUTOCOMPLETE_SCORE,
|
||||
SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
|
||||
} from 'src/SqlLab/constants';
|
||||
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
import {
|
||||
Editor,
|
||||
AceCompleterKeyword,
|
||||
FullSQLEditor as AceEditor,
|
||||
} from 'src/components/AsyncAceEditor';
|
||||
|
||||
type HotKey = {
|
||||
key: string;
|
||||
|
|
@ -61,7 +59,7 @@ interface Props {
|
|||
interface State {
|
||||
sql: string;
|
||||
selectedText: string;
|
||||
words: any[];
|
||||
words: AceCompleterKeyword[];
|
||||
}
|
||||
|
||||
class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||
|
|
@ -151,43 +149,6 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
|||
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 (!Number.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 || [];
|
||||
|
|
@ -229,20 +190,35 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
|||
meta: 'function',
|
||||
}));
|
||||
|
||||
const completer = {
|
||||
insertMatch: (editor: Editor, data: any) => {
|
||||
if (data.meta === 'table') {
|
||||
this.props.actions.addTable(
|
||||
this.props.queryEditor,
|
||||
data.value,
|
||||
this.props.queryEditor.schema,
|
||||
);
|
||||
}
|
||||
// executing https://github.com/thlorenz/brace/blob/3a00c5d59777f9d826841178e1eb36694177f5e6/ext/language_tools.js#L1448
|
||||
editor.completer.insertMatch(
|
||||
`${data.caption}${
|
||||
['function', 'schema'].includes(data.meta) ? '' : ' '
|
||||
}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const words = schemaWords
|
||||
.concat(tableWords)
|
||||
.concat(columnWords)
|
||||
.concat(functionWords)
|
||||
.concat(sqlKeywords);
|
||||
.concat(sqlKeywords)
|
||||
.map(word => ({
|
||||
...word,
|
||||
completer,
|
||||
}));
|
||||
|
||||
this.setState({ words }, () => {
|
||||
const completer = {
|
||||
getCompletions: this.getCompletions.bind(this),
|
||||
};
|
||||
if (langTools) {
|
||||
langTools.setCompleters([completer]);
|
||||
}
|
||||
});
|
||||
this.setState({ words });
|
||||
}
|
||||
|
||||
getAceAnnotations() {
|
||||
|
|
@ -263,8 +239,7 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
|||
render() {
|
||||
return (
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
theme="github"
|
||||
keywords={this.state.words}
|
||||
onLoad={this.onEditorLoad.bind(this)}
|
||||
onBlur={this.onBlur.bind(this)}
|
||||
height={this.props.height}
|
||||
|
|
|
|||
|
|
@ -19,18 +19,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Badge } from 'react-bootstrap';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/html';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
import { t } from '@superset-ui/core';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
|
||||
import Button from 'src/components/Button';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { ConfigEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
|
|
@ -104,9 +98,9 @@ export default class TemplateParamsEditor extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
{this.renderDoc()}
|
||||
<AceEditor
|
||||
<ConfigEditor
|
||||
keywords={[]}
|
||||
mode={this.props.language}
|
||||
theme="textmate"
|
||||
style={{ border: '1px solid #CCC' }}
|
||||
minLines={25}
|
||||
maxLines={50}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,215 @@
|
|||
/**
|
||||
* 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 {
|
||||
Editor as OrigEditor,
|
||||
IEditSession,
|
||||
Position,
|
||||
TextMode as OrigTextMode,
|
||||
} from 'brace';
|
||||
import AceEditor, { AceEditorProps } from 'react-ace';
|
||||
import AsyncEsmComponent, {
|
||||
PlaceholderProps,
|
||||
} from 'src/components/AsyncEsmComponent';
|
||||
|
||||
export interface AceCompleterKeywordData {
|
||||
name: string;
|
||||
value: string;
|
||||
score: number;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export type TextMode = OrigTextMode & { $id: string };
|
||||
|
||||
export interface AceCompleter {
|
||||
insertMatch: (
|
||||
data?: Editor | { value: string } | string,
|
||||
options?: AceCompleterKeywordData,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type Editor = OrigEditor & {
|
||||
completer: AceCompleter;
|
||||
completers: AceCompleter[];
|
||||
};
|
||||
|
||||
export interface AceCompleterKeyword extends AceCompleterKeywordData {
|
||||
completer?: AceCompleter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async loaders to import brace modules. Must manually create call `import(...)`
|
||||
* promises because webpack can only analyze asycn imports statically.
|
||||
*/
|
||||
const aceModuleLoaders = {
|
||||
'mode/sql': () => import('brace/mode/sql'),
|
||||
'mode/markdown': () => import('brace/mode/markdown'),
|
||||
'mode/css': () => import('brace/mode/css'),
|
||||
'mode/json': () => import('brace/mode/json'),
|
||||
'mode/yaml': () => import('brace/mode/yaml'),
|
||||
'mode/html': () => import('brace/mode/html'),
|
||||
'mode/javascript': () => import('brace/mode/javascript'),
|
||||
'theme/textmate': () => import('brace/theme/textmate'),
|
||||
'theme/github': () => import('brace/theme/github'),
|
||||
'ext/language_tools': () => import('brace/ext/language_tools'),
|
||||
};
|
||||
|
||||
export type AceModule = keyof typeof aceModuleLoaders;
|
||||
|
||||
export type AsyncAceEditorProps = AceEditorProps & {
|
||||
keywords?: AceCompleterKeyword[];
|
||||
};
|
||||
|
||||
export type AceEditorMode = 'sql';
|
||||
export type AceEditorTheme = 'textmate' | 'github';
|
||||
export type AsyncAceEditorOptions = {
|
||||
defaultMode?: AceEditorMode;
|
||||
defaultTheme?: AceEditorTheme;
|
||||
defaultTabSize?: number;
|
||||
placeholder?: React.ComponentType<
|
||||
PlaceholderProps & Partial<AceEditorProps>
|
||||
> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an async AceEditor with automatical loading of specified ace modules.
|
||||
*/
|
||||
export default function AsyncAceEditor(
|
||||
aceModules: AceModule[],
|
||||
{
|
||||
defaultMode,
|
||||
defaultTheme,
|
||||
defaultTabSize = 2,
|
||||
placeholder,
|
||||
}: AsyncAceEditorOptions = {},
|
||||
) {
|
||||
return AsyncEsmComponent(async () => {
|
||||
const { default: ace } = await import('brace');
|
||||
const { default: ReactAceEditor } = await import('react-ace');
|
||||
|
||||
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
|
||||
|
||||
const inferredMode =
|
||||
defaultMode ||
|
||||
aceModules.find(x => x.startsWith('mode/'))?.replace('mode/', '');
|
||||
const inferredTheme =
|
||||
defaultTheme ||
|
||||
aceModules.find(x => x.startsWith('theme/'))?.replace('theme/', '');
|
||||
|
||||
return React.forwardRef<AceEditor, AsyncAceEditorProps>(
|
||||
function ExtendedAceEditor(
|
||||
{
|
||||
keywords,
|
||||
mode = inferredMode,
|
||||
theme = inferredTheme,
|
||||
tabSize = defaultTabSize,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
if (keywords) {
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
const completer = {
|
||||
getCompletions: (
|
||||
editor: AceEditor,
|
||||
session: IEditSession,
|
||||
pos: Position,
|
||||
prefix: string,
|
||||
callback: (error: null, wordList: object[]) => void,
|
||||
) => {
|
||||
// If the prefix starts with a number, don't try to autocomplete
|
||||
if (!Number.isNaN(parseInt(prefix, 10))) {
|
||||
return;
|
||||
}
|
||||
if ((session.getMode() as TextMode).$id === `ace/mode/${mode}`) {
|
||||
callback(null, keywords);
|
||||
}
|
||||
},
|
||||
};
|
||||
langTools.setCompleters([completer]);
|
||||
}
|
||||
return (
|
||||
<ReactAceEditor
|
||||
ref={ref}
|
||||
mode={mode}
|
||||
theme={theme}
|
||||
tabSize={tabSize}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
}, placeholder);
|
||||
}
|
||||
|
||||
export const SQLEditor = AsyncAceEditor([
|
||||
'mode/sql',
|
||||
'theme/github',
|
||||
'ext/language_tools',
|
||||
]);
|
||||
|
||||
export const FullSQLEditor = AsyncAceEditor(
|
||||
['mode/sql', 'theme/github', 'ext/language_tools'],
|
||||
{
|
||||
// a custom placeholder in SQL lab for less jumpy re-renders
|
||||
placeholder: () => {
|
||||
const gutterBackground = '#e8e8e8'; // from ace-github theme
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: 41, height: '100%', background: gutterBackground }}
|
||||
/>
|
||||
{/* make it possible to resize the placeholder */}
|
||||
<div className="ace_content" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const MarkdownEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
'theme/textmate',
|
||||
]);
|
||||
|
||||
export const TextAreaEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
'mode/sql',
|
||||
'mode/json',
|
||||
'mode/html',
|
||||
'mode/javascript',
|
||||
'theme/textmate',
|
||||
]);
|
||||
|
||||
export const CssEditor = AsyncAceEditor(['mode/css', 'theme/github']);
|
||||
|
||||
export const JsonEditor = AsyncAceEditor(['mode/json', 'theme/github']);
|
||||
|
||||
/**
|
||||
* JSON or Yaml config editor.
|
||||
*/
|
||||
export const ConfigEditor = AsyncAceEditor([
|
||||
'mode/json',
|
||||
'mode/yaml',
|
||||
'theme/github',
|
||||
]);
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* 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, { CSSProperties, useEffect, useState, RefObject } from 'react';
|
||||
import Loading from './Loading';
|
||||
|
||||
export type PlaceholderProps = {
|
||||
showLoadingForImport?: boolean;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
placeholderStyle?: CSSProperties;
|
||||
} & {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
function DefaultPlaceholder({
|
||||
width,
|
||||
height,
|
||||
showLoadingForImport = false,
|
||||
placeholderStyle: style,
|
||||
}: PlaceholderProps) {
|
||||
return (
|
||||
// since `width` defaults to 100%, we can display the placeholder once
|
||||
// height is specified.
|
||||
(height && (
|
||||
<div key="async-asm-placeholder" style={{ width, height, ...style }}>
|
||||
{showLoadingForImport && <Loading position="floating" />}
|
||||
</div>
|
||||
)) ||
|
||||
// `|| null` is for in case of height=0.
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously import an ES module as a React component, render a placeholder
|
||||
* first (if provided) and re-render once import is complete.
|
||||
*/
|
||||
export default function AsyncEsmComponent<
|
||||
P = PlaceholderProps,
|
||||
M = React.ComponentType<P> | { default: React.ComponentType<P> }
|
||||
>(
|
||||
/**
|
||||
* A promise generator that returns the React component to render.
|
||||
*/
|
||||
loadComponent: Promise<M> | (() => Promise<M>),
|
||||
/**
|
||||
* Placeholder while still importing.
|
||||
*/
|
||||
placeholder: React.ComponentType<
|
||||
PlaceholderProps & Partial<P>
|
||||
> | null = DefaultPlaceholder,
|
||||
) {
|
||||
// component props + placeholder props
|
||||
type FullProps = P & PlaceholderProps;
|
||||
let promise: Promise<M> | undefined;
|
||||
let component: React.ComponentType<FullProps>;
|
||||
|
||||
/**
|
||||
* Safely wait for promise, make sure the loader function only execute once.
|
||||
*/
|
||||
function waitForPromise() {
|
||||
if (!promise) {
|
||||
// load component on initialization
|
||||
promise =
|
||||
loadComponent instanceof Promise ? loadComponent : loadComponent();
|
||||
}
|
||||
if (!component) {
|
||||
promise.then(result => {
|
||||
component = ((result as { default?: React.ComponentType<P> }).default ||
|
||||
result) as React.ComponentType<FullProps>;
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
type AsyncComponent = React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<FullProps> &
|
||||
React.RefAttributes<React.ComponentType<FullProps>>
|
||||
> & {
|
||||
preload?: typeof waitForPromise;
|
||||
};
|
||||
|
||||
const AsyncComponent: AsyncComponent = React.forwardRef(
|
||||
function AsyncComponent(
|
||||
props: FullProps,
|
||||
ref: RefObject<React.ComponentType<FullProps>>,
|
||||
) {
|
||||
const [loaded, setLoaded] = useState(component !== undefined);
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
if (!loaded) {
|
||||
// update state to trigger a re-render
|
||||
waitForPromise().then(() => {
|
||||
if (isMounted) {
|
||||
setLoaded(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
});
|
||||
const Component = component || placeholder;
|
||||
return Component ? (
|
||||
// placeholder does not get the ref
|
||||
<Component ref={Component === component ? ref : null} {...props} />
|
||||
) : null;
|
||||
},
|
||||
);
|
||||
// preload the async component before rendering
|
||||
AsyncComponent.preload = waitForPromise;
|
||||
|
||||
return AsyncComponent as AsyncComponent & {
|
||||
preload: typeof waitForPromise;
|
||||
};
|
||||
}
|
||||
|
|
@ -19,12 +19,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'src/components/Select';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/css';
|
||||
import 'brace/theme/github';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
const propTypes = {
|
||||
initialCss: PropTypes.string,
|
||||
|
|
@ -49,6 +46,10 @@ class CssEditor extends React.PureComponent {
|
|||
this.changeCssTemplate = this.changeCssTemplate.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AceCssEditor.preload();
|
||||
}
|
||||
|
||||
changeCss(css) {
|
||||
this.setState({ css }, () => {
|
||||
this.props.onChange(css);
|
||||
|
|
@ -87,10 +88,8 @@ class CssEditor extends React.PureComponent {
|
|||
<div style={{ zIndex: 1 }}>
|
||||
<h5>{t('Live CSS Editor')}</h5>
|
||||
<div style={{ border: 'solid 1px grey' }}>
|
||||
<AceEditor
|
||||
mode="css"
|
||||
theme="github"
|
||||
minLines={8}
|
||||
<AceCssEditor
|
||||
minLines={12}
|
||||
maxLines={30}
|
||||
onChange={this.changeCss}
|
||||
height="200px"
|
||||
|
|
|
|||
|
|
@ -23,14 +23,22 @@ import PropTypes from 'prop-types';
|
|||
import { styled, CategoricalColorNamespace, t } from '@superset-ui/core';
|
||||
import { ButtonGroup } from 'react-bootstrap';
|
||||
|
||||
import {
|
||||
LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD,
|
||||
LOG_ACTIONS_FORCE_REFRESH_DASHBOARD,
|
||||
LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD,
|
||||
} from 'src/logger/LogUtils';
|
||||
|
||||
import Icon from 'src/components/Icon';
|
||||
import Button from 'src/components/Button';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
|
||||
import HeaderActionsDropdown from './HeaderActionsDropdown';
|
||||
import EditableTitle from '../../components/EditableTitle';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
import PublishedStatus from './PublishedStatus';
|
||||
import UndoRedoKeylisteners from './UndoRedoKeylisteners';
|
||||
import PropertiesModal from './PropertiesModal';
|
||||
|
||||
import { chartPropShape } from '../util/propShapes';
|
||||
import {
|
||||
|
|
@ -38,14 +46,6 @@ import {
|
|||
SAVE_TYPE_OVERWRITE,
|
||||
DASHBOARD_POSITION_DATA_LIMIT,
|
||||
} from '../util/constants';
|
||||
import { safeStringify } from '../../utils/safeStringify';
|
||||
|
||||
import {
|
||||
LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD,
|
||||
LOG_ACTIONS_FORCE_REFRESH_DASHBOARD,
|
||||
LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD,
|
||||
} from '../../logger/LogUtils';
|
||||
import PropertiesModal from './PropertiesModal';
|
||||
import setPeriodicRunner from '../util/setPeriodicRunner';
|
||||
import { options as PeriodicRefreshOptions } from './RefreshIntervalModal';
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import { Row, Col, Modal, FormControl } from 'react-bootstrap';
|
|||
import Button from 'src/components/Button';
|
||||
import Dialog from 'react-bootstrap-dialog';
|
||||
import { AsyncSelect } from 'src/components/Select';
|
||||
import AceEditor from 'react-ace';
|
||||
import rison from 'rison';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
|
||||
import FormLabel from 'src/components/FormLabel';
|
||||
import { JsonEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
import ColorSchemeControlWrapper from 'src/dashboard/components/ColorSchemeControlWrapper';
|
||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||
import withToasts from '../../messageToasts/enhancers/withToasts';
|
||||
|
|
@ -79,6 +80,7 @@ class PropertiesModal extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.fetchDashboardDetails();
|
||||
JsonEditor.preload();
|
||||
}
|
||||
|
||||
onColorSchemeChange(value) {
|
||||
|
|
@ -304,13 +306,12 @@ class PropertiesModal extends React.PureComponent {
|
|||
<FormLabel htmlFor="json_metadata">
|
||||
{t('JSON Metadata')}
|
||||
</FormLabel>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
<JsonEditor
|
||||
showLoadingForImport
|
||||
name="json_metadata"
|
||||
defaultValue={this.defaultMetadataValue}
|
||||
value={values.json_metadata}
|
||||
onChange={this.onMetadataChange}
|
||||
theme="textmate"
|
||||
tabSize={2}
|
||||
width="100%"
|
||||
height="200px"
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import cx from 'classnames';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/theme/textmate';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
||||
import { MarkdownEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
import DeleteComponentButton from '../DeleteComponentButton';
|
||||
import DragDroppable from '../dnd/DragDroppable';
|
||||
|
|
@ -37,7 +36,6 @@ import {
|
|||
GRID_MIN_ROW_UNITS,
|
||||
GRID_BASE_UNIT,
|
||||
} from '../../util/constants';
|
||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger/LogUtils';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
|
@ -166,6 +164,10 @@ class Markdown extends React.PureComponent {
|
|||
) {
|
||||
this.state.editor.resize(true);
|
||||
}
|
||||
// pre-load AceEditor when entering edit mode
|
||||
if (this.props.editMode) {
|
||||
MarkdownEditor.preload();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch() {
|
||||
|
|
@ -230,9 +232,7 @@ class Markdown extends React.PureComponent {
|
|||
|
||||
renderEditMode() {
|
||||
return (
|
||||
<AceEditor
|
||||
mode="markdown"
|
||||
theme="textmate"
|
||||
<MarkdownEditor
|
||||
onChange={this.handleMarkdownChange}
|
||||
width="100%"
|
||||
height="100%"
|
||||
|
|
|
|||
|
|
@ -27,20 +27,22 @@ import Button from 'src/components/Button';
|
|||
import Loading from 'src/components/Loading';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
|
||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||
import CheckboxControl from '../explore/components/controls/CheckboxControl';
|
||||
import TextControl from '../explore/components/controls/TextControl';
|
||||
import SelectControl from '../explore/components/controls/SelectControl';
|
||||
import TextAreaControl from '../explore/components/controls/TextAreaControl';
|
||||
import SelectAsyncControl from '../explore/components/controls/SelectAsyncControl';
|
||||
import SpatialControl from '../explore/components/controls/SpatialControl';
|
||||
import CollectionTable from '../CRUD/CollectionTable';
|
||||
import EditableTitle from '../components/EditableTitle';
|
||||
import Fieldset from '../CRUD/Fieldset';
|
||||
import Field from '../CRUD/Field';
|
||||
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
||||
|
||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
||||
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
||||
import TextControl from 'src/explore/components/controls/TextControl';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||
import SelectAsyncControl from 'src/explore/components/controls/SelectAsyncControl';
|
||||
import SpatialControl from 'src/explore/components/controls/SpatialControl';
|
||||
|
||||
import CollectionTable from 'src/CRUD/CollectionTable';
|
||||
import Fieldset from 'src/CRUD/Fieldset';
|
||||
import Field from 'src/CRUD/Field';
|
||||
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
|
||||
const DatasourceContainer = styled.div`
|
||||
.tab-content {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ import Button from 'src/components/Button';
|
|||
// @ts-ignore
|
||||
import Dialog from 'react-bootstrap-dialog';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
|
||||
|
||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||
import DatasourceEditor from './DatasourceEditor';
|
||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
||||
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
|
||||
const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor'));
|
||||
|
||||
interface DatasourceModalProps {
|
||||
addSuccessToast: (msg: string) => void;
|
||||
|
|
@ -150,6 +152,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||
<Modal.Body>
|
||||
{show && (
|
||||
<DatasourceEditor
|
||||
showLoadingForImport
|
||||
height={500}
|
||||
datasource={currentDatasource}
|
||||
onChange={onDatasourceChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,16 +18,12 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AceEditor from 'react-ace';
|
||||
import ace from 'brace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
import { FormGroup } from 'react-bootstrap';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
|
||||
import sqlKeywords from '../../SqlLab/utils/sqlKeywords';
|
||||
import AdhocFilter, { EXPRESSION_TYPES, CLAUSES } from '../AdhocFilter';
|
||||
import adhocMetricType from '../propTypes/adhocMetricType';
|
||||
import columnType from '../propTypes/columnType';
|
||||
|
|
@ -45,8 +41,6 @@ const propTypes = {
|
|||
height: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
|
||||
export default class AdhocFilterEditPopoverSqlTabContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -63,32 +57,12 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
autosize: false,
|
||||
clearable: false,
|
||||
};
|
||||
|
||||
if (langTools) {
|
||||
const words = sqlKeywords.concat(
|
||||
this.props.options.map(option => {
|
||||
if (option.column_name) {
|
||||
return {
|
||||
name: option.column_name,
|
||||
value: option.column_name,
|
||||
score: 50,
|
||||
meta: 'option',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
const completer = {
|
||||
getCompletions: (aceEditor, session, pos, prefix, callback) => {
|
||||
callback(null, words);
|
||||
},
|
||||
};
|
||||
langTools.setCompleters([completer]);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.aceEditorRef.editor.resize();
|
||||
if (this.aceEditorRef) {
|
||||
this.aceEditorRef.editor.resize();
|
||||
}
|
||||
}
|
||||
|
||||
onSqlExpressionClauseChange(clause) {
|
||||
|
|
@ -116,7 +90,7 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
}
|
||||
|
||||
render() {
|
||||
const { adhocFilter, height } = this.props;
|
||||
const { adhocFilter, height, options } = this.props;
|
||||
|
||||
const clauseSelectProps = {
|
||||
placeholder: t('choose WHERE or HAVING...'),
|
||||
|
|
@ -124,6 +98,21 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
value: adhocFilter.clause,
|
||||
onChange: this.onSqlExpressionClauseChange,
|
||||
};
|
||||
const keywords = sqlKeywords.concat(
|
||||
options
|
||||
.map(option => {
|
||||
if (option.column_name) {
|
||||
return {
|
||||
name: option.column_name,
|
||||
value: option.column_name,
|
||||
score: 50,
|
||||
meta: 'option',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
|
|
@ -140,10 +129,9 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
</span>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<AceEditor
|
||||
<SQLEditor
|
||||
ref={this.handleAceEditorRef}
|
||||
mode="sql"
|
||||
theme="github"
|
||||
keywords={keywords}
|
||||
height={`${height - 100}px`}
|
||||
onChange={this.onSqlExpressionChange}
|
||||
width="100%"
|
||||
|
|
|
|||
|
|
@ -21,23 +21,17 @@ import PropTypes from 'prop-types';
|
|||
import { FormGroup, Popover, Tab, Tabs } from 'react-bootstrap';
|
||||
import Button from 'src/components/Button';
|
||||
import Select from 'src/components/Select';
|
||||
import ace from 'brace';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
import { t, ThemeProvider } from '@superset-ui/core';
|
||||
import { ColumnOption } from '@superset-ui/chart-controls';
|
||||
|
||||
import FormLabel from 'src/components/FormLabel';
|
||||
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
|
||||
import { AGGREGATES_OPTIONS } from '../constants';
|
||||
import AdhocMetricEditPopoverTitle from './AdhocMetricEditPopoverTitle';
|
||||
import columnType from '../propTypes/columnType';
|
||||
import AdhocMetric, { EXPRESSION_TYPES } from '../AdhocMetric';
|
||||
import sqlKeywords from '../../SqlLab/utils/sqlKeywords';
|
||||
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
|
||||
const propTypes = {
|
||||
adhocMetric: PropTypes.instanceOf(AdhocMetric).isRequired,
|
||||
|
|
@ -80,22 +74,6 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
autosize: false,
|
||||
clearable: true,
|
||||
};
|
||||
if (langTools) {
|
||||
const words = sqlKeywords.concat(
|
||||
this.props.columns.map(column => ({
|
||||
name: column.column_name,
|
||||
value: column.column_name,
|
||||
score: 50,
|
||||
meta: 'column',
|
||||
})),
|
||||
);
|
||||
const completer = {
|
||||
getCompletions: (aceEditor, session, pos, prefix, callback) => {
|
||||
callback(null, words);
|
||||
},
|
||||
};
|
||||
langTools.setCompleters([completer]);
|
||||
}
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +157,11 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
}
|
||||
|
||||
refreshAceEditor() {
|
||||
setTimeout(() => this.aceEditorRef.editor.resize(), 0);
|
||||
setTimeout(() => {
|
||||
if (this.aceEditorRef) {
|
||||
this.aceEditorRef.editor.resize();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
renderColumnOption(option) {
|
||||
|
|
@ -199,6 +181,14 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
} = this.props;
|
||||
|
||||
const { adhocMetric } = this.state;
|
||||
const keywords = sqlKeywords.concat(
|
||||
columns.map(column => ({
|
||||
name: column.column_name,
|
||||
value: column.column_name,
|
||||
score: 50,
|
||||
meta: 'column',
|
||||
})),
|
||||
);
|
||||
|
||||
const columnSelectProps = {
|
||||
placeholder: t('%s column(s)', columns.length),
|
||||
|
|
@ -279,10 +269,10 @@ export default class AdhocMetricEditPopover extends React.Component {
|
|||
>
|
||||
{this.props.datasourceType !== 'druid' ? (
|
||||
<FormGroup>
|
||||
<AceEditor
|
||||
<SQLEditor
|
||||
showLoadingForImport
|
||||
ref={this.handleAceEditorRef}
|
||||
mode="sql"
|
||||
theme="github"
|
||||
keywords={keywords}
|
||||
height={`${this.state.height - 43}px`}
|
||||
onChange={this.onSqlExpressionChange}
|
||||
width="100%"
|
||||
|
|
|
|||
|
|
@ -27,10 +27,15 @@ import {
|
|||
import { connect } from 'react-redux';
|
||||
import { t, withTheme } from '@superset-ui/core';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import { getChartKey } from '../../exploreUtils';
|
||||
import { runAnnotationQuery } from '../../../chart/chartAction';
|
||||
import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
|
||||
import { getChartKey } from 'src/explore/exploreUtils';
|
||||
import { runAnnotationQuery } from 'src/chart/chartAction';
|
||||
|
||||
import AnnotationLayer from './AnnotationLayer';
|
||||
const AnnotationLayer = AsyncEsmComponent(
|
||||
() => import('./AnnotationLayer'),
|
||||
// size of overlay inner content
|
||||
() => <div style={{ width: 450, height: 368 }} />,
|
||||
);
|
||||
|
||||
const propTypes = {
|
||||
colorScheme: PropTypes.string.isRequired,
|
||||
|
|
@ -61,6 +66,11 @@ class AnnotationLayerControl extends React.PureComponent {
|
|||
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// preload the AnotationLayer component and dependent libraries i.e. mathjs
|
||||
AnnotationLayer.preload();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { name, annotationError, validationErrors, value } = nextProps;
|
||||
if (Object.keys(annotationError).length && !validationErrors.length) {
|
||||
|
|
@ -111,6 +121,7 @@ class AnnotationLayerControl extends React.PureComponent {
|
|||
>
|
||||
<AnnotationLayer
|
||||
{...annotation}
|
||||
parent={this.refs[parent]}
|
||||
error={error}
|
||||
colorScheme={this.props.colorScheme}
|
||||
vizType={this.props.vizType}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,12 @@ import { t } from '@superset-ui/core';
|
|||
import { ColumnOption, MetricOption } from '@superset-ui/chart-controls';
|
||||
|
||||
import Label from 'src/components/Label';
|
||||
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||
|
||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
||||
import ChangeDatasourceModal from '../../../datasource/ChangeDatasourceModal';
|
||||
import TooltipWrapper from '../../../components/TooltipWrapper';
|
||||
import './DatasourceControl.less';
|
||||
|
||||
const propTypes = {
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import Button from 'src/components/Button';
|
|||
import { t } from '@superset-ui/core';
|
||||
|
||||
import Label from 'src/components/Label';
|
||||
import PopoverSection from 'src/components/PopoverSection';
|
||||
import Checkbox from 'src/components/Checkbox';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import SelectControl from './SelectControl';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
import Checkbox from '../../../components/Checkbox';
|
||||
|
||||
const spatialTypes = {
|
||||
latlong: 'latlong',
|
||||
|
|
|
|||
|
|
@ -19,20 +19,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/html';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
import Button from 'src/components/Button';
|
||||
import { TextAreaEditor } from 'src/components/AsyncAceEditor';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import ModalTrigger from '../../../components/ModalTrigger';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
|
|
@ -65,6 +59,12 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
export default class TextAreaControl extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onAceChangeDebounce = debounce(value => {
|
||||
this.onAceChange(value);
|
||||
}, 300);
|
||||
}
|
||||
onControlChange(event) {
|
||||
this.props.onChange(event.target.value);
|
||||
}
|
||||
|
|
@ -75,18 +75,18 @@ export default class TextAreaControl extends React.Component {
|
|||
|
||||
renderEditor(inModal = false) {
|
||||
const value = this.props.value || '';
|
||||
const minLines = inModal ? 40 : this.props.minLines || 12;
|
||||
if (this.props.language) {
|
||||
return (
|
||||
<AceEditor
|
||||
<TextAreaEditor
|
||||
mode={this.props.language}
|
||||
theme="textmate"
|
||||
style={{ border: '1px solid #CCC' }}
|
||||
minLines={inModal ? 40 : this.props.minLines}
|
||||
minLines={minLines}
|
||||
maxLines={inModal ? 1000 : this.props.maxLines}
|
||||
onChange={this.onAceChange.bind(this)}
|
||||
onChange={this.onAceChangeDebounce}
|
||||
width="100%"
|
||||
height={`${minLines}em`}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={value}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
|
||||
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
|
||||
|
||||
export const CHECK_DASHBOARD_FAVORITE_ENDPOINT =
|
||||
'/superset/favstar/Dashboard/*/count';
|
||||
declare module 'brace/mode/sql';
|
||||
declare module 'brace/mode/markdown';
|
||||
declare module 'brace/mode/json';
|
||||
declare module 'brace/mode/css';
|
||||
declare module 'brace/mode/html';
|
||||
declare module 'brace/mode/yaml';
|
||||
declare module 'brace/mode/javascript';
|
||||
declare module 'brace/theme/textmate';
|
||||
declare module 'brace/theme/github';
|
||||
declare module 'brace/ext/language_tools';
|
||||
|
|
@ -26,20 +26,20 @@ import { t, styled, SupersetClient } from '@superset-ui/core';
|
|||
|
||||
import FormLabel from 'src/components/FormLabel';
|
||||
|
||||
import DateFilterControl from '../../explore/components/controls/DateFilterControl';
|
||||
import ControlRow from '../../explore/components/ControlRow';
|
||||
import Control from '../../explore/components/Control';
|
||||
import controls from '../../explore/controls';
|
||||
import { getExploreUrl } from '../../explore/exploreUtils';
|
||||
import OnPasteSelect from '../../components/Select/OnPasteSelect';
|
||||
import { getDashboardFilterKey } from '../../dashboard/util/getDashboardFilterKey';
|
||||
import { getFilterColorMap } from '../../dashboard/util/dashboardFiltersColorMap';
|
||||
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
||||
import ControlRow from 'src/explore/components/ControlRow';
|
||||
import Control from 'src/explore/components/Control';
|
||||
import controls from 'src/explore/controls';
|
||||
import { getExploreUrl } from 'src/explore/exploreUtils';
|
||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||
import { getDashboardFilterKey } from 'src/dashboard/util/getDashboardFilterKey';
|
||||
import { getFilterColorMap } from 'src/dashboard/util/dashboardFiltersColorMap';
|
||||
import {
|
||||
FILTER_CONFIG_ATTRIBUTES,
|
||||
FILTER_OPTIONS_LIMIT,
|
||||
TIME_FILTER_LABELS,
|
||||
} from '../../explore/constants';
|
||||
import FilterBadgeIcon from '../../components/FilterBadgeIcon';
|
||||
} from 'src/explore/constants';
|
||||
import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
|
||||
|
||||
import './FilterBox.less';
|
||||
|
||||
|
|
|
|||
|
|
@ -55,9 +55,12 @@ const output = {
|
|||
if (isDevMode) {
|
||||
output.filename = '[name].[hash:8].entry.js';
|
||||
output.chunkFilename = '[name].[hash:8].chunk.js';
|
||||
} else {
|
||||
} else if (nameChunks) {
|
||||
output.filename = '[name].[chunkhash].entry.js';
|
||||
output.chunkFilename = '[name].[chunkhash].chunk.js';
|
||||
} else {
|
||||
output.filename = '[name].[chunkhash].entry.js';
|
||||
output.chunkFilename = '[chunkhash].chunk.js';
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
|
|
@ -199,6 +202,8 @@ const config = {
|
|||
sideEffects: true,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
// increase minSize for devMode to 1000kb because of sourcemap
|
||||
minSize: isDevMode ? 1000000 : 20000,
|
||||
name: nameChunks,
|
||||
automaticNameDelimiter: '-',
|
||||
minChunks: 2,
|
||||
|
|
@ -214,6 +219,8 @@ const config = {
|
|||
'react',
|
||||
'react-dom',
|
||||
'prop-types',
|
||||
'react-prop-types',
|
||||
'prop-types-extra',
|
||||
'redux',
|
||||
'react-redux',
|
||||
'react-hot-loader',
|
||||
|
|
@ -221,6 +228,7 @@ const config = {
|
|||
'react-sortable-hoc',
|
||||
'react-virtualized',
|
||||
'react-table',
|
||||
'react-ace',
|
||||
'@hot-loader.*',
|
||||
'webpack.*',
|
||||
'@?babel.*',
|
||||
|
|
@ -228,20 +236,17 @@ const config = {
|
|||
'antd',
|
||||
'@ant-design.*',
|
||||
'.*bootstrap',
|
||||
'react-bootstrap-slider',
|
||||
'moment',
|
||||
'jquery',
|
||||
'core-js.*',
|
||||
'@emotion.*',
|
||||
'd3.*',
|
||||
'd3',
|
||||
'd3-(array|color|scale|interpolate|format|selection|collection|time|time-format)',
|
||||
].join('|')})/`,
|
||||
),
|
||||
},
|
||||
// bundle large libraries separately
|
||||
brace: {
|
||||
name: 'brace',
|
||||
test: /\/node_modules\/(brace|react-ace)\//,
|
||||
priority: 40,
|
||||
},
|
||||
mathjs: {
|
||||
name: 'mathjs',
|
||||
test: /\/node_modules\/mathjs\//,
|
||||
|
|
|
|||
Loading…
Reference in New Issue