You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by su...@apache.org on 2020/08/10 23:31:39 UTC

[incubator-superset] 02/06: dynamic import working for explore

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 <aa...@gmail.com>
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,