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
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
import { WORLD_HEALTH_DASHBOARD, drag } from './dashboard.helper';
|
||||||
|
|
||||||
describe('Dashboard edit mode', () => {
|
describe('Dashboard edit mode', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -45,19 +45,9 @@ describe('Dashboard edit mode', () => {
|
||||||
.find('.chart-card-container')
|
.find('.chart-card-container')
|
||||||
.contains('Box plot');
|
.contains('Box plot');
|
||||||
|
|
||||||
// drag-n-drop
|
drag('.chart-card', 'Box plot').to(
|
||||||
const dataTransfer = { data: {} };
|
'.grid-row.background--transparent:last',
|
||||||
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 });
|
|
||||||
|
|
||||||
// add back to dashboard
|
// add back to dashboard
|
||||||
cy.get('.grid-container .box_plot').should('be.exist');
|
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');
|
cy.route('GET', '/superset/filter/table/*/name').as('filterValues');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Set simple adhoc filter', () => {
|
it('Should not load mathjs when not needed', () => {
|
||||||
cy.visitChartByName('Num Births Trend');
|
cy.visitChartByName('Boys'); // a table chart
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
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('[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('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('#filter-edit-popover').within(() => {
|
||||||
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
|
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
|
||||||
cy.get('.Select__control').click();
|
cy.get('.Select__control').click();
|
||||||
|
|
@ -40,7 +57,6 @@ describe('AdhocFilters', () => {
|
||||||
});
|
});
|
||||||
cy.get('button').contains('Save').click();
|
cy.get('button').contains('Save').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('button[data-test="run-query-button"]').click();
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
waitAlias: '@postJson',
|
waitAlias: '@postJson',
|
||||||
|
|
@ -52,19 +68,21 @@ describe('AdhocFilters', () => {
|
||||||
cy.visitChartByName('Num Births Trend');
|
cy.visitChartByName('Num Births Trend');
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||||
|
|
||||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
cy.get('[data-test=adhoc_filters] .Select__control')
|
||||||
cy.get('.Select__control').click();
|
.scrollIntoView()
|
||||||
cy.get('input[type=text]').focus().type('name{enter}');
|
.click();
|
||||||
});
|
cy.get('[data-test=adhoc_filters] input[type=text]')
|
||||||
|
.focus()
|
||||||
|
.type('name{enter}');
|
||||||
|
|
||||||
cy.wait('@filterValues');
|
cy.wait('@filterValues');
|
||||||
|
|
||||||
cy.get('#filter-edit-popover').within(() => {
|
cy.get('#filter-edit-popover #adhoc-filter-edit-tabs-tab-SQL').click();
|
||||||
cy.get('#adhoc-filter-edit-tabs-tab-SQL').click();
|
cy.get('#filter-edit-popover .ace_content').click();
|
||||||
cy.get('.ace_content').click();
|
cy.get('#filter-edit-popover .ace_text-input').type(
|
||||||
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'");
|
"'Amy' OR name = 'Bob'",
|
||||||
cy.get('button').contains('Save').click();
|
);
|
||||||
});
|
cy.get('#filter-edit-popover button').contains('Save').click();
|
||||||
|
|
||||||
cy.get('button[data-test="run-query-button"]').click();
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
cy.verifySliceSuccess({
|
cy.verifySliceSuccess({
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,25 @@ describe('Datasource control', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow edit datasource', () => {
|
it('should allow edit datasource', () => {
|
||||||
|
let numScripts = 0;
|
||||||
|
|
||||||
cy.visitChartByName('Num Births Trend');
|
cy.visitChartByName('Num Births Trend');
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||||
cy.get('#datasource_menu').click();
|
cy.get('#datasource_menu').click();
|
||||||
|
|
||||||
|
cy.get('script').then(nodes => {
|
||||||
|
numScripts = nodes.length;
|
||||||
|
});
|
||||||
|
|
||||||
cy.get('a').contains('Edit Datasource').click();
|
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
|
// 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>"]').click();
|
||||||
cy.get('input[value="<new metric>"]')
|
cy.get('input[value="<new metric>"]')
|
||||||
.focus()
|
.focus()
|
||||||
|
|
@ -65,19 +78,33 @@ describe('Datasource control', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Groupby control', () => {
|
describe('VizType control', () => {
|
||||||
it('Set groupby', () => {
|
beforeEach(() => {
|
||||||
cy.server();
|
|
||||||
cy.login();
|
cy.login();
|
||||||
|
cy.server();
|
||||||
cy.route('GET', '/superset/explore_json/**').as('getJson');
|
cy.route('GET', '/superset/explore_json/**').as('getJson');
|
||||||
cy.route('POST', '/superset/explore_json/**').as('postJson');
|
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.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||||
|
|
||||||
cy.get('[data-test=groupby]').within(() => {
|
let numScripts = 0;
|
||||||
cy.get('.Select__control').click();
|
cy.get('script').then(nodes => {
|
||||||
cy.get('input[type=text]').type('state{enter}');
|
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.get('button[data-test="run-query-button"]').click();
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@postJson', chartSelector: 'svg' });
|
||||||
});
|
});
|
||||||
|
|
@ -118,3 +145,21 @@ describe('Time range filter', () => {
|
||||||
cy.get('#filter-popover').should('not.exist');
|
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`);
|
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', () => {
|
it('should not show validator error when metric added', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
|
|
@ -68,6 +76,7 @@ describe('Visualization > Line', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||||
|
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with groupby', () => {
|
it('should work with groupby', () => {
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ import { Provider } from 'react-redux';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import AceEditor from 'react-ace';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
import { MarkdownEditor } from 'src/components/AsyncAceEditor';
|
||||||
import Markdown from 'src/dashboard/components/gridComponents/Markdown';
|
import Markdown from 'src/dashboard/components/gridComponents/Markdown';
|
||||||
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
|
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
|
||||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||||
|
|
@ -105,23 +105,23 @@ describe('Markdown', () => {
|
||||||
|
|
||||||
it('should render an Markdown when NOT focused', () => {
|
it('should render an Markdown when NOT focused', () => {
|
||||||
const wrapper = setup();
|
const wrapper = setup();
|
||||||
expect(wrapper.find(AceEditor)).not.toExist();
|
expect(wrapper.find(MarkdownEditor)).not.toExist();
|
||||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
expect(wrapper.find(ReactMarkdown)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render an AceEditor when focused and editMode=true and editorMode=edit', () => {
|
it('should render an AceEditor when focused and editMode=true and editorMode=edit', () => {
|
||||||
const wrapper = setup({ editMode: true });
|
const wrapper = setup({ editMode: true });
|
||||||
expect(wrapper.find(AceEditor)).not.toExist();
|
expect(wrapper.find(MarkdownEditor)).not.toExist();
|
||||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
expect(wrapper.find(ReactMarkdown)).toExist();
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||||
expect(wrapper.find(AceEditor)).toExist();
|
expect(wrapper.find(MarkdownEditor)).toExist();
|
||||||
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a ReactMarkdown when focused and editMode=true and editorMode=preview', () => {
|
it('should render a ReactMarkdown when focused and editMode=true and editorMode=preview', () => {
|
||||||
const wrapper = setup({ editMode: true });
|
const wrapper = setup({ editMode: true });
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||||
expect(wrapper.find(AceEditor)).toExist();
|
expect(wrapper.find(MarkdownEditor)).toExist();
|
||||||
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
expect(wrapper.find(ReactMarkdown)).not.toExist();
|
||||||
|
|
||||||
// we can't call setState on Markdown bc it's not the root component, so call
|
// we can't call setState on Markdown bc it's not the root component, so call
|
||||||
|
|
@ -131,7 +131,7 @@ describe('Markdown', () => {
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find(ReactMarkdown)).toExist();
|
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', () => {
|
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');
|
dropdown.prop('onChange')('edit');
|
||||||
// because we can't call setState on Markdown, change it through the editor
|
// because we can't call setState on Markdown, change it through the editor
|
||||||
// then go back to preview mode to invoke updateComponents
|
// then go back to preview mode to invoke updateComponents
|
||||||
const editor = wrapper.find(AceEditor);
|
const editor = wrapper.find(MarkdownEditor);
|
||||||
editor.prop('onChange')('new markdown!');
|
editor.prop('onChange')('new markdown!');
|
||||||
dropdown.prop('onChange')('preview');
|
dropdown.prop('onChange')('preview');
|
||||||
expect(updateComponents.callCount).toBe(1);
|
expect(updateComponents.callCount).toBe(1);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
||||||
import { FormControl } from 'react-bootstrap';
|
import { FormControl } from 'react-bootstrap';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import AceEditor from 'react-ace';
|
import { TextAreaEditor } from 'src/components/AsyncAceEditor';
|
||||||
|
|
||||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||||
|
|
||||||
|
|
@ -52,6 +52,6 @@ describe('SelectControl', () => {
|
||||||
props.language = 'markdown';
|
props.language = 'markdown';
|
||||||
wrapper = shallow(<TextAreaControl {...props} />);
|
wrapper = shallow(<TextAreaControl {...props} />);
|
||||||
expect(wrapper.find(FormControl)).not.toExist();
|
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}
|
{extraButtons}
|
||||||
{allowDeletes && !allowAddItem && (
|
{allowDeletes && !allowAddItem && (
|
||||||
<th aria-label="Delete" className="tiny-cell" />
|
<th key="delete-item" aria-label="Delete" className="tiny-cell" />
|
||||||
)}
|
)}
|
||||||
{allowAddItem && (
|
{allowAddItem && (
|
||||||
<th>
|
<th key="add-item">
|
||||||
<Button buttonStyle="primary" onClick={this.onAddItem}>
|
<Button buttonStyle="primary" onClick={this.onAddItem}>
|
||||||
<i className="fa fa-plus" /> {t('Add Item')}
|
<i className="fa fa-plus" /> {t('Add Item')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -237,7 +237,7 @@ export default class CRUDCollection extends React.PureComponent<
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
if (allowAddItem) {
|
if (allowAddItem) {
|
||||||
tds.push(<td />);
|
tds.push(<td key="add" />);
|
||||||
}
|
}
|
||||||
if (allowDeletes) {
|
if (allowDeletes) {
|
||||||
tds.push(
|
tds.push(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,6 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
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 { areArraysShallowEqual } from 'src/reduxUtils';
|
||||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,8 +25,11 @@ import {
|
||||||
COLUMN_AUTOCOMPLETE_SCORE,
|
COLUMN_AUTOCOMPLETE_SCORE,
|
||||||
SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
|
SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
|
||||||
} from 'src/SqlLab/constants';
|
} from 'src/SqlLab/constants';
|
||||||
|
import {
|
||||||
const langTools = ace.acequire('ace/ext/language_tools');
|
Editor,
|
||||||
|
AceCompleterKeyword,
|
||||||
|
FullSQLEditor as AceEditor,
|
||||||
|
} from 'src/components/AsyncAceEditor';
|
||||||
|
|
||||||
type HotKey = {
|
type HotKey = {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -61,7 +59,7 @@ interface Props {
|
||||||
interface State {
|
interface State {
|
||||||
sql: string;
|
sql: string;
|
||||||
selectedText: string;
|
selectedText: string;
|
||||||
words: any[];
|
words: AceCompleterKeyword[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AceEditorWrapper extends React.PureComponent<Props, State> {
|
class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||||
|
|
@ -151,43 +149,6 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||||
this.props.onChange(text);
|
this.props.onChange(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(
|
|
||||||
aceEditor: any,
|
|
||||||
session: any,
|
|
||||||
pos: any,
|
|
||||||
prefix: string,
|
|
||||||
callback: (p0: any, p1: any[]) => void,
|
|
||||||
) {
|
|
||||||
// If the prefix starts with a number, don't try to autocomplete with a
|
|
||||||
// table name or schema or anything else
|
|
||||||
if (!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) {
|
setAutoCompleter(props: Props) {
|
||||||
// Loading schema, table and column names as auto-completable words
|
// Loading schema, table and column names as auto-completable words
|
||||||
const schemas = props.schemas || [];
|
const schemas = props.schemas || [];
|
||||||
|
|
@ -229,20 +190,35 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||||
meta: 'function',
|
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
|
const words = schemaWords
|
||||||
.concat(tableWords)
|
.concat(tableWords)
|
||||||
.concat(columnWords)
|
.concat(columnWords)
|
||||||
.concat(functionWords)
|
.concat(functionWords)
|
||||||
.concat(sqlKeywords);
|
.concat(sqlKeywords)
|
||||||
|
.map(word => ({
|
||||||
|
...word,
|
||||||
|
completer,
|
||||||
|
}));
|
||||||
|
|
||||||
this.setState({ words }, () => {
|
this.setState({ words });
|
||||||
const completer = {
|
|
||||||
getCompletions: this.getCompletions.bind(this),
|
|
||||||
};
|
|
||||||
if (langTools) {
|
|
||||||
langTools.setCompleters([completer]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAceAnnotations() {
|
getAceAnnotations() {
|
||||||
|
|
@ -263,8 +239,7 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode="sql"
|
keywords={this.state.words}
|
||||||
theme="github"
|
|
||||||
onLoad={this.onEditorLoad.bind(this)}
|
onLoad={this.onEditorLoad.bind(this)}
|
||||||
onBlur={this.onBlur.bind(this)}
|
onBlur={this.onBlur.bind(this)}
|
||||||
height={this.props.height}
|
height={this.props.height}
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Badge } from 'react-bootstrap';
|
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 { t } from '@superset-ui/core';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
import Button from 'src/components/Button';
|
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 = {
|
const propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
|
@ -104,9 +98,9 @@ export default class TemplateParamsEditor extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.renderDoc()}
|
{this.renderDoc()}
|
||||||
<AceEditor
|
<ConfigEditor
|
||||||
|
keywords={[]}
|
||||||
mode={this.props.language}
|
mode={this.props.language}
|
||||||
theme="textmate"
|
|
||||||
style={{ border: '1px solid #CCC' }}
|
style={{ border: '1px solid #CCC' }}
|
||||||
minLines={25}
|
minLines={25}
|
||||||
maxLines={50}
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Select from 'src/components/Select';
|
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 { t } from '@superset-ui/core';
|
||||||
|
import ModalTrigger from 'src/components/ModalTrigger';
|
||||||
import ModalTrigger from '../../components/ModalTrigger';
|
import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
initialCss: PropTypes.string,
|
initialCss: PropTypes.string,
|
||||||
|
|
@ -49,6 +46,10 @@ class CssEditor extends React.PureComponent {
|
||||||
this.changeCssTemplate = this.changeCssTemplate.bind(this);
|
this.changeCssTemplate = this.changeCssTemplate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
AceCssEditor.preload();
|
||||||
|
}
|
||||||
|
|
||||||
changeCss(css) {
|
changeCss(css) {
|
||||||
this.setState({ css }, () => {
|
this.setState({ css }, () => {
|
||||||
this.props.onChange(css);
|
this.props.onChange(css);
|
||||||
|
|
@ -87,10 +88,8 @@ class CssEditor extends React.PureComponent {
|
||||||
<div style={{ zIndex: 1 }}>
|
<div style={{ zIndex: 1 }}>
|
||||||
<h5>{t('Live CSS Editor')}</h5>
|
<h5>{t('Live CSS Editor')}</h5>
|
||||||
<div style={{ border: 'solid 1px grey' }}>
|
<div style={{ border: 'solid 1px grey' }}>
|
||||||
<AceEditor
|
<AceCssEditor
|
||||||
mode="css"
|
minLines={12}
|
||||||
theme="github"
|
|
||||||
minLines={8}
|
|
||||||
maxLines={30}
|
maxLines={30}
|
||||||
onChange={this.changeCss}
|
onChange={this.changeCss}
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,22 @@ import PropTypes from 'prop-types';
|
||||||
import { styled, CategoricalColorNamespace, t } from '@superset-ui/core';
|
import { styled, CategoricalColorNamespace, t } from '@superset-ui/core';
|
||||||
import { ButtonGroup } from 'react-bootstrap';
|
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 Icon from 'src/components/Icon';
|
||||||
import Button from 'src/components/Button';
|
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 HeaderActionsDropdown from './HeaderActionsDropdown';
|
||||||
import EditableTitle from '../../components/EditableTitle';
|
|
||||||
import FaveStar from '../../components/FaveStar';
|
|
||||||
import PublishedStatus from './PublishedStatus';
|
import PublishedStatus from './PublishedStatus';
|
||||||
import UndoRedoKeylisteners from './UndoRedoKeylisteners';
|
import UndoRedoKeylisteners from './UndoRedoKeylisteners';
|
||||||
|
import PropertiesModal from './PropertiesModal';
|
||||||
|
|
||||||
import { chartPropShape } from '../util/propShapes';
|
import { chartPropShape } from '../util/propShapes';
|
||||||
import {
|
import {
|
||||||
|
|
@ -38,14 +46,6 @@ import {
|
||||||
SAVE_TYPE_OVERWRITE,
|
SAVE_TYPE_OVERWRITE,
|
||||||
DASHBOARD_POSITION_DATA_LIMIT,
|
DASHBOARD_POSITION_DATA_LIMIT,
|
||||||
} from '../util/constants';
|
} 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 setPeriodicRunner from '../util/setPeriodicRunner';
|
||||||
import { options as PeriodicRefreshOptions } from './RefreshIntervalModal';
|
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 Button from 'src/components/Button';
|
||||||
import Dialog from 'react-bootstrap-dialog';
|
import Dialog from 'react-bootstrap-dialog';
|
||||||
import { AsyncSelect } from 'src/components/Select';
|
import { AsyncSelect } from 'src/components/Select';
|
||||||
import AceEditor from 'react-ace';
|
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { t, SupersetClient } from '@superset-ui/core';
|
import { t, SupersetClient } from '@superset-ui/core';
|
||||||
|
|
||||||
import FormLabel from 'src/components/FormLabel';
|
import FormLabel from 'src/components/FormLabel';
|
||||||
|
import { JsonEditor } from 'src/components/AsyncAceEditor';
|
||||||
|
|
||||||
import ColorSchemeControlWrapper from 'src/dashboard/components/ColorSchemeControlWrapper';
|
import ColorSchemeControlWrapper from 'src/dashboard/components/ColorSchemeControlWrapper';
|
||||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||||
import withToasts from '../../messageToasts/enhancers/withToasts';
|
import withToasts from '../../messageToasts/enhancers/withToasts';
|
||||||
|
|
@ -79,6 +80,7 @@ class PropertiesModal extends React.PureComponent {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchDashboardDetails();
|
this.fetchDashboardDetails();
|
||||||
|
JsonEditor.preload();
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorSchemeChange(value) {
|
onColorSchemeChange(value) {
|
||||||
|
|
@ -304,13 +306,12 @@ class PropertiesModal extends React.PureComponent {
|
||||||
<FormLabel htmlFor="json_metadata">
|
<FormLabel htmlFor="json_metadata">
|
||||||
{t('JSON Metadata')}
|
{t('JSON Metadata')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<AceEditor
|
<JsonEditor
|
||||||
mode="json"
|
showLoadingForImport
|
||||||
name="json_metadata"
|
name="json_metadata"
|
||||||
defaultValue={this.defaultMetadataValue}
|
defaultValue={this.defaultMetadataValue}
|
||||||
value={values.json_metadata}
|
value={values.json_metadata}
|
||||||
onChange={this.onMetadataChange}
|
onChange={this.onMetadataChange}
|
||||||
theme="textmate"
|
|
||||||
tabSize={2}
|
tabSize={2}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import AceEditor from 'react-ace';
|
|
||||||
import 'brace/mode/markdown';
|
|
||||||
import 'brace/theme/textmate';
|
|
||||||
import { t } from '@superset-ui/core';
|
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 DeleteComponentButton from '../DeleteComponentButton';
|
||||||
import DragDroppable from '../dnd/DragDroppable';
|
import DragDroppable from '../dnd/DragDroppable';
|
||||||
|
|
@ -37,7 +36,6 @@ import {
|
||||||
GRID_MIN_ROW_UNITS,
|
GRID_MIN_ROW_UNITS,
|
||||||
GRID_BASE_UNIT,
|
GRID_BASE_UNIT,
|
||||||
} from '../../util/constants';
|
} from '../../util/constants';
|
||||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger/LogUtils';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
|
@ -166,6 +164,10 @@ class Markdown extends React.PureComponent {
|
||||||
) {
|
) {
|
||||||
this.state.editor.resize(true);
|
this.state.editor.resize(true);
|
||||||
}
|
}
|
||||||
|
// pre-load AceEditor when entering edit mode
|
||||||
|
if (this.props.editMode) {
|
||||||
|
MarkdownEditor.preload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch() {
|
componentDidCatch() {
|
||||||
|
|
@ -230,9 +232,7 @@ class Markdown extends React.PureComponent {
|
||||||
|
|
||||||
renderEditMode() {
|
renderEditMode() {
|
||||||
return (
|
return (
|
||||||
<AceEditor
|
<MarkdownEditor
|
||||||
mode="markdown"
|
|
||||||
theme="textmate"
|
|
||||||
onChange={this.handleMarkdownChange}
|
onChange={this.handleMarkdownChange}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,22 @@ import Button from 'src/components/Button';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import TableSelector from 'src/components/TableSelector';
|
import TableSelector from 'src/components/TableSelector';
|
||||||
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||||
|
import EditableTitle from 'src/components/EditableTitle';
|
||||||
|
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
import getClientErrorObject from 'src/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 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`
|
const DatasourceContainer = styled.div`
|
||||||
.tab-content {
|
.tab-content {
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@ import Button from 'src/components/Button';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Dialog from 'react-bootstrap-dialog';
|
import Dialog from 'react-bootstrap-dialog';
|
||||||
import { t, SupersetClient } from '@superset-ui/core';
|
import { t, SupersetClient } from '@superset-ui/core';
|
||||||
|
import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
|
||||||
|
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
||||||
import DatasourceEditor from './DatasourceEditor';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
|
||||||
|
const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor'));
|
||||||
|
|
||||||
interface DatasourceModalProps {
|
interface DatasourceModalProps {
|
||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
|
|
@ -150,6 +152,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{show && (
|
{show && (
|
||||||
<DatasourceEditor
|
<DatasourceEditor
|
||||||
|
showLoadingForImport
|
||||||
|
height={500}
|
||||||
datasource={currentDatasource}
|
datasource={currentDatasource}
|
||||||
onChange={onDatasourceChange}
|
onChange={onDatasourceChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,12 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { FormGroup } from 'react-bootstrap';
|
||||||
import Select from 'src/components/Select';
|
import Select from 'src/components/Select';
|
||||||
import { t } from '@superset-ui/core';
|
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 AdhocFilter, { EXPRESSION_TYPES, CLAUSES } from '../AdhocFilter';
|
||||||
import adhocMetricType from '../propTypes/adhocMetricType';
|
import adhocMetricType from '../propTypes/adhocMetricType';
|
||||||
import columnType from '../propTypes/columnType';
|
import columnType from '../propTypes/columnType';
|
||||||
|
|
@ -45,8 +41,6 @@ const propTypes = {
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const langTools = ace.acequire('ace/ext/language_tools');
|
|
||||||
|
|
||||||
export default class AdhocFilterEditPopoverSqlTabContent extends React.Component {
|
export default class AdhocFilterEditPopoverSqlTabContent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -63,32 +57,12 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
||||||
autosize: false,
|
autosize: false,
|
||||||
clearable: 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() {
|
componentDidUpdate() {
|
||||||
this.aceEditorRef.editor.resize();
|
if (this.aceEditorRef) {
|
||||||
|
this.aceEditorRef.editor.resize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSqlExpressionClauseChange(clause) {
|
onSqlExpressionClauseChange(clause) {
|
||||||
|
|
@ -116,7 +90,7 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { adhocFilter, height } = this.props;
|
const { adhocFilter, height, options } = this.props;
|
||||||
|
|
||||||
const clauseSelectProps = {
|
const clauseSelectProps = {
|
||||||
placeholder: t('choose WHERE or HAVING...'),
|
placeholder: t('choose WHERE or HAVING...'),
|
||||||
|
|
@ -124,6 +98,21 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
||||||
value: adhocFilter.clause,
|
value: adhocFilter.clause,
|
||||||
onChange: this.onSqlExpressionClauseChange,
|
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 (
|
return (
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -140,10 +129,9 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
||||||
</span>
|
</span>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<AceEditor
|
<SQLEditor
|
||||||
ref={this.handleAceEditorRef}
|
ref={this.handleAceEditorRef}
|
||||||
mode="sql"
|
keywords={keywords}
|
||||||
theme="github"
|
|
||||||
height={`${height - 100}px`}
|
height={`${height - 100}px`}
|
||||||
onChange={this.onSqlExpressionChange}
|
onChange={this.onSqlExpressionChange}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|
|
||||||
|
|
@ -21,23 +21,17 @@ import PropTypes from 'prop-types';
|
||||||
import { FormGroup, Popover, Tab, Tabs } from 'react-bootstrap';
|
import { FormGroup, Popover, Tab, Tabs } from 'react-bootstrap';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import Select from 'src/components/Select';
|
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 { t, ThemeProvider } from '@superset-ui/core';
|
||||||
import { ColumnOption } from '@superset-ui/chart-controls';
|
import { ColumnOption } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
import FormLabel from 'src/components/FormLabel';
|
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 { AGGREGATES_OPTIONS } from '../constants';
|
||||||
import AdhocMetricEditPopoverTitle from './AdhocMetricEditPopoverTitle';
|
import AdhocMetricEditPopoverTitle from './AdhocMetricEditPopoverTitle';
|
||||||
import columnType from '../propTypes/columnType';
|
import columnType from '../propTypes/columnType';
|
||||||
import AdhocMetric, { EXPRESSION_TYPES } from '../AdhocMetric';
|
import AdhocMetric, { EXPRESSION_TYPES } from '../AdhocMetric';
|
||||||
import sqlKeywords from '../../SqlLab/utils/sqlKeywords';
|
|
||||||
|
|
||||||
const langTools = ace.acequire('ace/ext/language_tools');
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
adhocMetric: PropTypes.instanceOf(AdhocMetric).isRequired,
|
adhocMetric: PropTypes.instanceOf(AdhocMetric).isRequired,
|
||||||
|
|
@ -80,22 +74,6 @@ export default class AdhocMetricEditPopover extends React.Component {
|
||||||
autosize: false,
|
autosize: false,
|
||||||
clearable: true,
|
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);
|
document.addEventListener('mouseup', this.onMouseUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +157,11 @@ export default class AdhocMetricEditPopover extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshAceEditor() {
|
refreshAceEditor() {
|
||||||
setTimeout(() => this.aceEditorRef.editor.resize(), 0);
|
setTimeout(() => {
|
||||||
|
if (this.aceEditorRef) {
|
||||||
|
this.aceEditorRef.editor.resize();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderColumnOption(option) {
|
renderColumnOption(option) {
|
||||||
|
|
@ -199,6 +181,14 @@ export default class AdhocMetricEditPopover extends React.Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { adhocMetric } = this.state;
|
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 = {
|
const columnSelectProps = {
|
||||||
placeholder: t('%s column(s)', columns.length),
|
placeholder: t('%s column(s)', columns.length),
|
||||||
|
|
@ -279,10 +269,10 @@ export default class AdhocMetricEditPopover extends React.Component {
|
||||||
>
|
>
|
||||||
{this.props.datasourceType !== 'druid' ? (
|
{this.props.datasourceType !== 'druid' ? (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<AceEditor
|
<SQLEditor
|
||||||
|
showLoadingForImport
|
||||||
ref={this.handleAceEditorRef}
|
ref={this.handleAceEditorRef}
|
||||||
mode="sql"
|
keywords={keywords}
|
||||||
theme="github"
|
|
||||||
height={`${this.state.height - 43}px`}
|
height={`${this.state.height - 43}px`}
|
||||||
onChange={this.onSqlExpressionChange}
|
onChange={this.onSqlExpressionChange}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,15 @@ import {
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { t, withTheme } from '@superset-ui/core';
|
import { t, withTheme } from '@superset-ui/core';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
import { getChartKey } from '../../exploreUtils';
|
import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
|
||||||
import { runAnnotationQuery } from '../../../chart/chartAction';
|
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 = {
|
const propTypes = {
|
||||||
colorScheme: PropTypes.string.isRequired,
|
colorScheme: PropTypes.string.isRequired,
|
||||||
|
|
@ -61,6 +66,11 @@ class AnnotationLayerControl extends React.PureComponent {
|
||||||
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// preload the AnotationLayer component and dependent libraries i.e. mathjs
|
||||||
|
AnnotationLayer.preload();
|
||||||
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
const { name, annotationError, validationErrors, value } = nextProps;
|
const { name, annotationError, validationErrors, value } = nextProps;
|
||||||
if (Object.keys(annotationError).length && !validationErrors.length) {
|
if (Object.keys(annotationError).length && !validationErrors.length) {
|
||||||
|
|
@ -111,6 +121,7 @@ class AnnotationLayerControl extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<AnnotationLayer
|
<AnnotationLayer
|
||||||
{...annotation}
|
{...annotation}
|
||||||
|
parent={this.refs[parent]}
|
||||||
error={error}
|
error={error}
|
||||||
colorScheme={this.props.colorScheme}
|
colorScheme={this.props.colorScheme}
|
||||||
vizType={this.props.vizType}
|
vizType={this.props.vizType}
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,12 @@ import { t } from '@superset-ui/core';
|
||||||
import { ColumnOption, MetricOption } from '@superset-ui/chart-controls';
|
import { ColumnOption, MetricOption } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
import Label from 'src/components/Label';
|
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 ControlHeader from '../ControlHeader';
|
||||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
|
||||||
import ChangeDatasourceModal from '../../../datasource/ChangeDatasourceModal';
|
|
||||||
import TooltipWrapper from '../../../components/TooltipWrapper';
|
|
||||||
import './DatasourceControl.less';
|
import './DatasourceControl.less';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ import Button from 'src/components/Button';
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
|
|
||||||
import Label from 'src/components/Label';
|
import Label from 'src/components/Label';
|
||||||
|
import PopoverSection from 'src/components/PopoverSection';
|
||||||
|
import Checkbox from 'src/components/Checkbox';
|
||||||
import ControlHeader from '../ControlHeader';
|
import ControlHeader from '../ControlHeader';
|
||||||
import SelectControl from './SelectControl';
|
import SelectControl from './SelectControl';
|
||||||
import PopoverSection from '../../../components/PopoverSection';
|
|
||||||
import Checkbox from '../../../components/Checkbox';
|
|
||||||
|
|
||||||
const spatialTypes = {
|
const spatialTypes = {
|
||||||
latlong: 'latlong',
|
latlong: 'latlong',
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
import { FormGroup, FormControl } from 'react-bootstrap';
|
||||||
import Button from 'src/components/Button';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
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 { t } from '@superset-ui/core';
|
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 ControlHeader from '../ControlHeader';
|
||||||
import ModalTrigger from '../../../components/ModalTrigger';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
|
@ -65,6 +59,12 @@ const defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TextAreaControl extends React.Component {
|
export default class TextAreaControl extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.onAceChangeDebounce = debounce(value => {
|
||||||
|
this.onAceChange(value);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
onControlChange(event) {
|
onControlChange(event) {
|
||||||
this.props.onChange(event.target.value);
|
this.props.onChange(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
@ -75,18 +75,18 @@ export default class TextAreaControl extends React.Component {
|
||||||
|
|
||||||
renderEditor(inModal = false) {
|
renderEditor(inModal = false) {
|
||||||
const value = this.props.value || '';
|
const value = this.props.value || '';
|
||||||
|
const minLines = inModal ? 40 : this.props.minLines || 12;
|
||||||
if (this.props.language) {
|
if (this.props.language) {
|
||||||
return (
|
return (
|
||||||
<AceEditor
|
<TextAreaEditor
|
||||||
mode={this.props.language}
|
mode={this.props.language}
|
||||||
theme="textmate"
|
|
||||||
style={{ border: '1px solid #CCC' }}
|
style={{ border: '1px solid #CCC' }}
|
||||||
minLines={inModal ? 40 : this.props.minLines}
|
minLines={minLines}
|
||||||
maxLines={inModal ? 1000 : this.props.maxLines}
|
maxLines={inModal ? 1000 : this.props.maxLines}
|
||||||
onChange={this.onAceChange.bind(this)}
|
onChange={this.onAceChangeDebounce}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
height={`${minLines}em`}
|
||||||
editorProps={{ $blockScrolling: true }}
|
editorProps={{ $blockScrolling: true }}
|
||||||
enableLiveAutocompletion
|
|
||||||
value={value}
|
value={value}
|
||||||
readOnly={this.props.readOnly}
|
readOnly={this.props.readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
|
declare module 'brace/mode/sql';
|
||||||
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
|
declare module 'brace/mode/markdown';
|
||||||
|
declare module 'brace/mode/json';
|
||||||
export const CHECK_DASHBOARD_FAVORITE_ENDPOINT =
|
declare module 'brace/mode/css';
|
||||||
'/superset/favstar/Dashboard/*/count';
|
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 FormLabel from 'src/components/FormLabel';
|
||||||
|
|
||||||
import DateFilterControl from '../../explore/components/controls/DateFilterControl';
|
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
||||||
import ControlRow from '../../explore/components/ControlRow';
|
import ControlRow from 'src/explore/components/ControlRow';
|
||||||
import Control from '../../explore/components/Control';
|
import Control from 'src/explore/components/Control';
|
||||||
import controls from '../../explore/controls';
|
import controls from 'src/explore/controls';
|
||||||
import { getExploreUrl } from '../../explore/exploreUtils';
|
import { getExploreUrl } from 'src/explore/exploreUtils';
|
||||||
import OnPasteSelect from '../../components/Select/OnPasteSelect';
|
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
||||||
import { getDashboardFilterKey } from '../../dashboard/util/getDashboardFilterKey';
|
import { getDashboardFilterKey } from 'src/dashboard/util/getDashboardFilterKey';
|
||||||
import { getFilterColorMap } from '../../dashboard/util/dashboardFiltersColorMap';
|
import { getFilterColorMap } from 'src/dashboard/util/dashboardFiltersColorMap';
|
||||||
import {
|
import {
|
||||||
FILTER_CONFIG_ATTRIBUTES,
|
FILTER_CONFIG_ATTRIBUTES,
|
||||||
FILTER_OPTIONS_LIMIT,
|
FILTER_OPTIONS_LIMIT,
|
||||||
TIME_FILTER_LABELS,
|
TIME_FILTER_LABELS,
|
||||||
} from '../../explore/constants';
|
} from 'src/explore/constants';
|
||||||
import FilterBadgeIcon from '../../components/FilterBadgeIcon';
|
import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
|
||||||
|
|
||||||
import './FilterBox.less';
|
import './FilterBox.less';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,12 @@ const output = {
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
output.filename = '[name].[hash:8].entry.js';
|
output.filename = '[name].[hash:8].entry.js';
|
||||||
output.chunkFilename = '[name].[hash:8].chunk.js';
|
output.chunkFilename = '[name].[hash:8].chunk.js';
|
||||||
} else {
|
} else if (nameChunks) {
|
||||||
output.filename = '[name].[chunkhash].entry.js';
|
output.filename = '[name].[chunkhash].entry.js';
|
||||||
output.chunkFilename = '[name].[chunkhash].chunk.js';
|
output.chunkFilename = '[name].[chunkhash].chunk.js';
|
||||||
|
} else {
|
||||||
|
output.filename = '[name].[chunkhash].entry.js';
|
||||||
|
output.chunkFilename = '[chunkhash].chunk.js';
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
|
|
@ -199,6 +202,8 @@ const config = {
|
||||||
sideEffects: true,
|
sideEffects: true,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
chunks: 'all',
|
chunks: 'all',
|
||||||
|
// increase minSize for devMode to 1000kb because of sourcemap
|
||||||
|
minSize: isDevMode ? 1000000 : 20000,
|
||||||
name: nameChunks,
|
name: nameChunks,
|
||||||
automaticNameDelimiter: '-',
|
automaticNameDelimiter: '-',
|
||||||
minChunks: 2,
|
minChunks: 2,
|
||||||
|
|
@ -214,6 +219,8 @@ const config = {
|
||||||
'react',
|
'react',
|
||||||
'react-dom',
|
'react-dom',
|
||||||
'prop-types',
|
'prop-types',
|
||||||
|
'react-prop-types',
|
||||||
|
'prop-types-extra',
|
||||||
'redux',
|
'redux',
|
||||||
'react-redux',
|
'react-redux',
|
||||||
'react-hot-loader',
|
'react-hot-loader',
|
||||||
|
|
@ -221,6 +228,7 @@ const config = {
|
||||||
'react-sortable-hoc',
|
'react-sortable-hoc',
|
||||||
'react-virtualized',
|
'react-virtualized',
|
||||||
'react-table',
|
'react-table',
|
||||||
|
'react-ace',
|
||||||
'@hot-loader.*',
|
'@hot-loader.*',
|
||||||
'webpack.*',
|
'webpack.*',
|
||||||
'@?babel.*',
|
'@?babel.*',
|
||||||
|
|
@ -228,20 +236,17 @@ const config = {
|
||||||
'antd',
|
'antd',
|
||||||
'@ant-design.*',
|
'@ant-design.*',
|
||||||
'.*bootstrap',
|
'.*bootstrap',
|
||||||
|
'react-bootstrap-slider',
|
||||||
'moment',
|
'moment',
|
||||||
'jquery',
|
'jquery',
|
||||||
'core-js.*',
|
'core-js.*',
|
||||||
'@emotion.*',
|
'@emotion.*',
|
||||||
'd3.*',
|
'd3',
|
||||||
|
'd3-(array|color|scale|interpolate|format|selection|collection|time|time-format)',
|
||||||
].join('|')})/`,
|
].join('|')})/`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// bundle large libraries separately
|
// bundle large libraries separately
|
||||||
brace: {
|
|
||||||
name: 'brace',
|
|
||||||
test: /\/node_modules\/(brace|react-ace)\//,
|
|
||||||
priority: 40,
|
|
||||||
},
|
|
||||||
mathjs: {
|
mathjs: {
|
||||||
name: 'mathjs',
|
name: 'mathjs',
|
||||||
test: /\/node_modules\/mathjs\//,
|
test: /\/node_modules\/mathjs\//,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue