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,