This is an automated email from the ASF dual-hosted git repository. suddjian pushed a commit to branch dynamic-plugin-import in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit fee32479c5f30d4b5f919ed062a13abbaa69b9c8 Author: David Aaron Suddjian <[email protected]> AuthorDate: Wed Jul 1 14:48:33 2020 -0700 dynamic import working for explore --- .../DynamicPlugins/DynamicPluginProvider.tsx | 90 ++++++++++++++++------ .../src/components/DynamicPlugins/PluginContext.ts | 8 +- superset-frontend/src/explore/App.jsx | 11 ++- .../explore/components/ControlPanelsContainer.jsx | 16 ++++ .../explore/components/ExploreViewContainer.jsx | 9 +++ .../explore/components/controls/VizTypeControl.jsx | 26 +++++-- .../src/utils/getClientErrorObject.ts | 17 ++-- 7 files changed, 131 insertions(+), 46 deletions(-) diff --git a/superset-frontend/src/components/DynamicPlugins/DynamicPluginProvider.tsx b/superset-frontend/src/components/DynamicPlugins/DynamicPluginProvider.tsx index ca45e66..4e5da20 100644 --- a/superset-frontend/src/components/DynamicPlugins/DynamicPluginProvider.tsx +++ b/superset-frontend/src/components/DynamicPlugins/DynamicPluginProvider.tsx @@ -1,37 +1,79 @@ import React, { useEffect, useState } from 'react'; -// use scriptjs for browser-side dynamic importing -// import $script from 'scriptjs'; -// import { Preset } from '@superset-ui/core'; -import PluginContext, { initialPluginContext } from './PluginContext'; - -console.log('from superset:', React); +import { + PluginContext, + initialPluginContext, + LoadingStatus, +} from './PluginContext'; // In future this should be provided by an api call const pluginUrls = ['http://localhost:8080/main.js']; +// TODO: Make this function an export of @superset-ui/chart or some such +async function defineSharedModule(name: string, promise: Promise<any>) { + // dependency management using global variables, because for the life of me + // I can't figure out how to hook into UMD from a dynamically imported package. + // Maybe someone else can help figure that out. + const loadingKey = '__superset__loading__/' + name; + const pkgKey = '__superset__/' + name; + if (window[loadingKey]) { + await window[loadingKey]; + return window[pkgKey]; + } + window[loadingKey] = promise; + const pkg = await promise; + window[pkgKey] = pkg; + return pkg; +} + +async function defineSharedModules(moduleMap: { [key: string]: Promise<any> }) { + return Promise.all( + Object.entries(moduleMap).map(([name, promise]) => { + defineSharedModule(name, promise); + }), + ); +} + export type Props = React.PropsWithChildren<{}>; export default function DynamicPluginProvider({ children }: Props) { - const [pluginState] = useState(initialPluginContext); + const [pluginState, setPluginState] = useState(initialPluginContext); useEffect(() => { - console.log('importing test'); - // $script(pluginUrls, () => { - // console.log('done'); - // }); - Promise.all( - pluginUrls.map(async url => { - const { default: d } = await import(/* webpackIgnore: true */ url); - return d; - }), - ).then(pluginModules => { - console.log(pluginModules); - // return new Preset({ - // name: 'Dynamic Charts', - // presets: [], - // plugins: [pluginModules], - // }); - }); + (async function () { + try { + await defineSharedModules({ + react: import('react'), + lodash: import('lodash'), + 'react-dom': import('react-dom'), + '@superset-ui/chart': import('@superset-ui/chart'), + '@superset-ui/chart-controls': import('@superset-ui/chart-controls'), + '@superset-ui/connection': import('@superset-ui/connection'), + '@superset-ui/color': import('@superset-ui/color'), + '@superset-ui/core': import('@superset-ui/core'), + '@superset-ui/dimension': import('@superset-ui/dimension'), + '@superset-ui/query': import('@superset-ui/query'), + '@superset-ui/style': import('@superset-ui/style'), + '@superset-ui/translation': import('@superset-ui/translation'), + '@superset-ui/validator': import('@superset-ui/validator'), + }); + + await Promise.all( + pluginUrls.map(url => import(/* webpackIgnore: true */ url)), + ); + + setPluginState({ + status: LoadingStatus.COMPLETE, + error: null, + }); + } catch (error) { + console.error(error.stack || error); + setPluginState({ + status: LoadingStatus.ERROR, + error, + }); + } + })(); }, [pluginUrls]); + return ( <PluginContext.Provider value={pluginState}> {children} diff --git a/superset-frontend/src/components/DynamicPlugins/PluginContext.ts b/superset-frontend/src/components/DynamicPlugins/PluginContext.ts index 100e813..d8e8080 100644 --- a/superset-frontend/src/components/DynamicPlugins/PluginContext.ts +++ b/superset-frontend/src/components/DynamicPlugins/PluginContext.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; export enum LoadingStatus { LOADING = 'loading', @@ -11,15 +11,13 @@ export type PluginContextType = { error: null | { message: string; }; - pluginKeys: string[]; }; export const initialPluginContext: PluginContextType = { status: LoadingStatus.LOADING, error: null, - pluginKeys: [], }; -const PluginContext = React.createContext(initialPluginContext); +export const PluginContext = React.createContext(initialPluginContext); -export default PluginContext; +export const useDynamicPluginContext = () => useContext(PluginContext); diff --git a/superset-frontend/src/explore/App.jsx b/superset-frontend/src/explore/App.jsx index e1f8b83..c84fad0 100644 --- a/superset-frontend/src/explore/App.jsx +++ b/superset-frontend/src/explore/App.jsx @@ -20,6 +20,7 @@ import React from 'react'; import { hot } from 'react-hot-loader/root'; import { Provider } from 'react-redux'; import { supersetTheme, ThemeProvider } from '@superset-ui/style'; +import DynamicPluginProvider from 'src/components/DynamicPlugins/DynamicPluginProvider'; import ToastPresenter from '../messageToasts/containers/ToastPresenter'; import ExploreViewContainer from './components/ExploreViewContainer'; import setupApp from '../setup/setupApp'; @@ -33,10 +34,12 @@ setupPlugins(); const App = ({ store }) => ( <Provider store={store}> <ThemeProvider theme={supersetTheme}> - <> - <ExploreViewContainer /> - <ToastPresenter /> - </> + <DynamicPluginProvider> + <> + <ExploreViewContainer /> + <ToastPresenter /> + </> + </DynamicPluginProvider> </ThemeProvider> </Provider> ); diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx index c3c175b..420d683 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx @@ -25,6 +25,10 @@ import { Alert, Tab, Tabs } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; import styled from '@superset-ui/style'; +import { + PluginContext, + LoadingStatus, +} from 'src/components/DynamicPlugins/PluginContext'; import ControlPanelSection from './ControlPanelSection'; import ControlRow from './ControlRow'; import Control from './Control'; @@ -61,6 +65,9 @@ const Styles = styled.div` `; class ControlPanelsContainer extends React.Component { + // trigger updates to the component when async plugins load + static contextType = PluginContext; + constructor(props) { super(props); @@ -167,6 +174,15 @@ class ControlPanelsContainer extends React.Component { } render() { + const cpRegistry = getChartControlPanelRegistry(); + if ( + !cpRegistry.has(this.props.form_data.viz_type) && + this.context.status === LoadingStatus.LOADING + ) { + // TODO replace with a snazzy loading spinner + return 'loading...'; + } + const querySectionsToRender = []; const displaySectionsToRender = []; this.sectionsToRender().forEach(section => { diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx index 6104216..93902a3 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx @@ -24,6 +24,10 @@ import { connect } from 'react-redux'; import styled from '@superset-ui/style'; import { t } from '@superset-ui/translation'; +import { + PluginContext, + LoadingStatus, +} from 'src/components/DynamicPlugins/PluginContext'; import ExploreChartPanel from './ExploreChartPanel'; import ControlPanelsContainer from './ControlPanelsContainer'; import SaveModal from './SaveModal'; @@ -77,6 +81,8 @@ const Styles = styled.div` `; class ExploreViewContainer extends React.Component { + static contextType = PluginContext; // eslint-disable-line react/sort-comp + constructor(props) { super(props); @@ -328,6 +334,9 @@ class ExploreViewContainer extends React.Component { } render() { + if (this.context.status === LoadingStatus.LOADING) { + return 'loading...'; + } if (this.props.standalone) { return this.renderChartContainer(); } diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx index 2c67f68..c626c9a 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { Label, @@ -32,6 +32,10 @@ import { getChartMetadataRegistry } from '@superset-ui/chart'; import ControlHeader from '../ControlHeader'; import './VizTypeControl.less'; +import { + useDynamicPluginContext, + LoadingStatus, +} from 'src/components/DynamicPlugins/PluginContext'; const propTypes = { description: PropTypes.string, @@ -101,6 +105,19 @@ const DEFAULT_ORDER = [ const typesWithDefaultOrder = new Set(DEFAULT_ORDER); +function VizSupportWarning({ registry, vizType }) { + const state = useDynamicPluginContext(); + if (state.status === LoadingStatus.LOADING || registry.has(vizType)) { + return null; + } + return ( + <div className="text-danger"> + <i className="fa fa-exclamation-circle text-danger" />{' '} + <small>{t('This visualization type is not supported.')}</small> + </div> + ); +} + export default class VizTypeControl extends React.PureComponent { constructor(props) { super(props); @@ -203,12 +220,7 @@ export default class VizTypeControl extends React.PureComponent { <Label onClick={this.toggleModal} style={LABEL_STYLE}> {registry.has(value) ? registry.get(value).name : `${value}`} </Label> - {!registry.has(value) && ( - <div className="text-danger"> - <i className="fa fa-exclamation-circle text-danger" />{' '} - <small>{t('This visualization type is not supported.')}</small> - </div> - )} + <VizSupportWarning registry={registry} vizType={value} /> </> </OverlayTrigger> <Modal diff --git a/superset-frontend/src/utils/getClientErrorObject.ts b/superset-frontend/src/utils/getClientErrorObject.ts index 6349496..cbc3efe 100644 --- a/superset-frontend/src/utils/getClientErrorObject.ts +++ b/superset-frontend/src/utils/getClientErrorObject.ts @@ -35,8 +35,12 @@ export type ClientErrorObject = { stacktrace?: string; } & Partial<SupersetClientResponse>; +interface ResponseWithTimeout extends Response { + timeout: number; +} + export default function getClientErrorObject( - response: SupersetClientResponse | (Response & { timeout: number }) | string, + response: SupersetClientResponse | ResponseWithTimeout | string, ): Promise<ClientErrorObject> { // takes a SupersetClientResponse as input, attempts to read response as Json if possible, // and returns a Promise that resolves to a plain object with error key and text value. @@ -52,7 +56,7 @@ export default function getClientErrorObject( responseObject .clone() .json() - .then(errorJson => { + .then((errorJson: any) => { let error = { ...responseObject, ...errorJson }; // Backwards compatibility for old error renderers with the new error object @@ -83,7 +87,7 @@ export default function getClientErrorObject( }) .catch(() => { // fall back to reading as text - responseObject.text().then(errorText => { + responseObject.text().then((errorText: any) => { resolve({ ...responseObject, error: errorText }); }); }); @@ -122,10 +126,11 @@ export default function getClientErrorObject( }); } else { // fall back to Response.statusText or generic error of we cannot read the response + console.error('non-standard error:', response); const error = - 'statusText' in response - ? response.statusText - : t('An error occurred'); + (response as any).statusText || + (response as any).message || + t('An error occurred'); resolve({ ...responseObject, error,
