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:
Jesse Yang 2020-09-15 14:12:06 -07:00 committed by GitHub
parent 5d529fd844
commit 0129c4253d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 764 additions and 268 deletions

View File

@ -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 });
},
};
}

View File

@ -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');

View File

@ -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');
});
});

View File

@ -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({

View File

@ -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' });
});
});

View File

@ -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', () => {

View File

@ -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);

View File

@ -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();
});
});

View File

@ -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(

View File

@ -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}

View File

@ -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}

View File

@ -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',
]);

View File

@ -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;
};
}

View File

@ -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"

View File

@ -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';

View File

@ -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"

View File

@ -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%"

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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%"

View File

@ -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%"

View File

@ -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}

View File

@ -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 = {

View File

@ -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',

View File

@ -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}
/>

View File

@ -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';

View File

@ -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';

View File

@ -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\//,