Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Dynamically imported viz plugins #10288

Merged
merged 69 commits into from
Dec 19, 2020
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
fba6456
first attempts at dynamic plugin loading
suddjian Jun 10, 2020
ad81f0b
dynamic import working for explore
suddjian Jul 1, 2020
8d7eaea
memoize appropriately
suddjian Jul 2, 2020
9613e63
add a backend for dynamic plugins
suddjian Jul 10, 2020
6c15e2d
hack at getting dynamic plugins working with dashboards
suddjian Jul 10, 2020
d245784
more work on making it work, + feature flag
suddjian Jul 10, 2020
aad4757
lint
suddjian Aug 14, 2020
c7061a5
actions to fix explore state when plugins load
suddjian Aug 14, 2020
0333f26
handle dynamic control panel, functionify ExploreViewContainer
suddjian Aug 14, 2020
81e835c
fix: rearrange migrations branch
Aug 17, 2020
eb44f49
fix: name and key as strings with length 50
Aug 17, 2020
24e81a7
bundle url length 2000
Aug 17, 2020
0e10da0
bundle url to text
Aug 17, 2020
f02cba0
fix: too long varchart
Aug 17, 2020
747d2b7
fix: pre-commit typing
Aug 17, 2020
d368338
fix: licenses
Aug 17, 2020
8b3f7b2
fix: add slice container was not initing feature flags
Aug 19, 2020
08b501c
fix: undo linting issue
Aug 19, 2020
d881071
fix: adjust down revision again
Aug 21, 2020
4251d9f
fix: adjust down revision again
Aug 21, 2020
a0dc11d
isort
suddjian Aug 28, 2020
0523d51
pylint
suddjian Aug 28, 2020
c0ee964
god damn linters
suddjian Aug 28, 2020
871ab2e
remove unnecessary(?) loading message
suddjian Aug 28, 2020
0e4abdf
only log non-standard errors
suddjian Aug 28, 2020
b0a0767
testing
suddjian Aug 31, 2020
5c71fa0
python is terrible
suddjian Sep 1, 2020
fac39bb
see above commit message
suddjian Sep 1, 2020
4b2d8d5
fix imports in DynamicPluginProvider
suddjian Dec 16, 2020
7062457
fix
suddjian Dec 17, 2020
4a04787
shift migration forward
suddjian Dec 17, 2020
22ddfde
lint
suddjian Dec 17, 2020
d5aa0d8
fix form data calculations to handle missing control config
suddjian Dec 17, 2020
4061622
temp commit - waiting for superset-ui changes and crud fixes
suddjian Dec 17, 2020
7c172b5
remove unnecessary todo
suddjian Dec 17, 2020
9401c8c
use new superset-ui shared module function
suddjian Dec 17, 2020
ef386dd
fetch the plugins instead of hardcoding the test one
suddjian Dec 17, 2020
5814335
Merge branch 'master' into dynamic-plugin-import
suddjian Dec 18, 2020
f7a0873
migration sort
suddjian Dec 18, 2020
680feb2
remove duplicated import statement
suddjian Dec 18, 2020
32ecf2d
format
suddjian Dec 18, 2020
7624937
try moving the import 🙄
suddjian Dec 18, 2020
06f0633
copy
suddjian Dec 18, 2020
62b7411
fix frontend tests
suddjian Dec 18, 2020
286f804
Merge branch 'master' into dynamic-plugin-import
suddjian Dec 18, 2020
85bd4f8
safe access
suddjian Dec 18, 2020
f14d694
comment out dead code
suddjian Dec 18, 2020
f5228dc
isort
suddjian Dec 18, 2020
c5821c2
disable pylint on necessary lines
suddjian Dec 18, 2020
5ec8977
use @superset-ui/logging instead of console
suddjian Dec 18, 2020
680e188
remove temp code
suddjian Dec 18, 2020
efc4a94
rearrange some code
suddjian Dec 18, 2020
7c962b3
try triggering mouseover in cypress before click
suddjian Dec 18, 2020
e9b88ca
use loading spinner instead of text
suddjian Dec 18, 2020
41905f7
trying to fix cypress
suddjian Dec 18, 2020
f2af077
attempt cypress fix
suddjian Dec 18, 2020
f93dcfc
customize permissions
suddjian Dec 18, 2020
2cefb61
Merge branch 'master' into dynamic-plugin-import
suddjian Dec 19, 2020
cff308d
Merge branch 'master' into dynamic-plugin-import
suddjian Dec 19, 2020
a6ce952
update package lock
suddjian Dec 19, 2020
c51e4ac
only admins can write to plugins by default
suddjian Dec 19, 2020
080675f
better copy
suddjian Dec 19, 2020
5caac5d
disable flaky tests
suddjian Dec 19, 2020
10f7a0d
use makeApi
suddjian Dec 19, 2020
0a8b10a
flaky tests
suddjian Dec 19, 2020
4f23ee6
cleanup code
suddjian Dec 19, 2020
2547086
flaaaakkkyyyyyy
suddjian Dec 19, 2020
7e637ab
dry
suddjian Dec 19, 2020
d0212d2
Merge branch 'master' into dynamic-plugin-import
suddjian Dec 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,306 changes: 2,181 additions & 125 deletions superset-frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@data-ui/sparkline": "^0.0.84",
"@emotion/core": "^10.0.35",
"@superset-ui/chart-controls": "^0.15.18",
"@superset-ui/core": "^0.15.18",
"@superset-ui/core": "^0.15.19",
"@superset-ui/legacy-plugin-chart-calendar": "^0.15.18",
"@superset-ui/legacy-plugin-chart-chord": "^0.15.18",
"@superset-ui/legacy-plugin-chart-country-map": "^0.15.18",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import React from 'react';
import * as ReactAll from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import sinon from 'sinon';
import { Subscription } from 'react-redux';
import { shallow } from 'enzyme';

Expand Down Expand Up @@ -88,33 +87,4 @@ describe('ExploreViewContainer', () => {
it('renders ChartContainer', () => {
expect(wrapper.find(ChartContainer)).toExist();
});

describe('UNSAFE_componentWillReceiveProps()', () => {
it('when controls change, should call resetControls', () => {
expect(wrapper.instance().props.controls.viz_type.value).toBe('table');
const resetControls = sinon.stub(
wrapper.instance().props.actions,
'resetControls',
);
const triggerQuery = sinon.stub(
wrapper.instance().props.actions,
'triggerQuery',
);

// triggers UNSAFE_componentWillReceiveProps
wrapper.setProps({
controls: {
viz_type: {
value: 'bar',
},
},
});
expect(resetControls.callCount).toBe(1);
// exploreview container should not force chart run query
// it should be controlled by redux state.
expect(triggerQuery.callCount).toBe(0);
resetControls.reset();
triggerQuery.reset();
});
});
});
8 changes: 7 additions & 1 deletion superset-frontend/src/addSlice/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { hot } from 'react-hot-loader/root';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import setupApp from '../setup/setupApp';
import setupPlugins from '../setup/setupPlugins';
import DynamicPluginProvider from '../components/DynamicPlugins/DynamicPluginProvider';
import AddSliceContainer from './AddSliceContainer';
import { initFeatureFlags } from '../featureFlags';

setupApp();
setupPlugins();
Expand All @@ -31,9 +33,13 @@ const bootstrapData = JSON.parse(
addSliceContainer?.getAttribute('data-bootstrap') || '{}',
);

initFeatureFlags(bootstrapData.common.feature_flags);

const App = () => (
<ThemeProvider theme={supersetTheme}>
<AddSliceContainer datasources={bootstrapData.datasources} />
<DynamicPluginProvider>
<AddSliceContainer datasources={bootstrapData.datasources} />
</DynamicPluginProvider>
</ThemeProvider>
);

Expand Down
15 changes: 15 additions & 0 deletions superset-frontend/src/chart/chartAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/* eslint no-param-reassign: ["error", { "props": false }] */
import moment from 'moment';
import { t, SupersetClient } from '@superset-ui/core';
import { getControlsState } from 'src/explore/store';
import { isFeatureEnabled, FeatureFlag } from '../featureFlags';
import {
getAnnotationJsonUrl,
Expand Down Expand Up @@ -101,6 +102,20 @@ export function annotationQueryFailed(annotation, queryResponse, key) {
return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
}

export const DYNAMIC_PLUGIN_CONTROLS_READY = 'DYNAMIC_PLUGIN_CONTROLS_READY';
export const dynamicPluginControlsReady = () => (dispatch, getState) => {
const state = getState();
const controlsState = getControlsState(
state.explore,
state.explore.form_data,
);
dispatch({
type: DYNAMIC_PLUGIN_CONTROLS_READY,
key: controlsState.slice_id.value,
controlsState,
});
};

const legacyChartDataRequest = async (
formData,
resultFormat,
Expand Down
5 changes: 5 additions & 0 deletions superset-frontend/src/chart/chartReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
/* eslint camelcase: 0 */
import { t } from '@superset-ui/core';
import { getFormDataFromControls } from 'src/explore/controlUtils';
import { now } from '../modules/dates';
import * as actions from './chartAction';

Expand Down Expand Up @@ -107,6 +108,10 @@ export default function chartReducer(charts = {}, action) {
: null,
};
},
[actions.DYNAMIC_PLUGIN_CONTROLS_READY](state) {
const sliceFormData = getFormDataFromControls(action.controlsState);
return { ...state, sliceFormData };
},
[actions.TRIGGER_QUERY](state) {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* 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, { useEffect, useReducer } from 'react';
import { defineSharedModules, SupersetClient } from '@superset-ui/core';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
dummyPluginContext,
PluginContext,
PluginContextType,
} from './PluginContext';

// the plugin returned from the API
type Plugin = {
name: string;
key: string;
bundle_url: string;
id: number;
};

type CompleteAction = {
type: 'complete';
key: string;
error: null | Error;
};

type BeginAction = {
type: 'begin';
keys: string[];
};

function pluginContextReducer(
state: PluginContextType,
action: BeginAction | CompleteAction,
): PluginContextType {
switch (action.type) {
case 'begin': {
const plugins = { ...state.plugins };
action.keys.forEach(key => {
plugins[key] = { key, error: null, loading: true };
});
return {
...state,
loading: true,
plugins,
};
}
case 'complete': {
return {
...state,
loading: Object.values(state.plugins).some(
plugin => plugin.loading && plugin.key !== action.key,
),
plugins: {
...state.plugins,
[action.key]: {
key: action.key,
loading: false,
error: action.error,
},
},
};
}
default:
return state;
}
}

export type Props = React.PropsWithChildren<{}>;

const sharedModules = {
react: () => import('react'),
lodash: () => import('lodash'),
'react-dom': () => import('react-dom'),
'@superset-ui/chart-controls': () => import('@superset-ui/chart-controls'),
'@superset-ui/core': () => import('@superset-ui/core'),
};

export default function DynamicPluginProvider({ children }: Props) {
const [pluginState, dispatch] = useReducer(pluginContextReducer, {
// use the dummy plugin context, and override the methods
...dummyPluginContext,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
fetchAll,
loading: isFeatureEnabled(FeatureFlag.DYNAMIC_PLUGINS),
// TODO: Write fetchByKeys
});

async function fetchAll() {
try {
await defineSharedModules(sharedModules);
const response = await SupersetClient.get({
endpoint: '/dynamic-plugins/api/read',
});
const plugins: Plugin[] = response.json.result;
// const plugins: Plugin[] = [
// {
// name: 'Hello World',
// key: 'superset-chart-hello-world',
// id: 0,
// bundle_url: 'http://127.0.0.1:8080/main.js',
// },
// ];
dispatch({ type: 'begin', keys: plugins.map(plugin => plugin.key) });
await Promise.all(
plugins.map(async plugin => {
let error: Error | null = null;
try {
await import(/* webpackIgnore: true */ plugin.bundle_url);
} catch (err) {
// eslint-disable-next-line no-console
console.error(
`Failed to load plugin ${plugin.key} with the following error:`,
err.stack,
);
error = err;
}
dispatch({
type: 'complete',
key: plugin.key,
error,
});
}),
);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error.stack || error);
suddjian marked this conversation as resolved.
Show resolved Hide resolved
}
}

useEffect(() => {
if (isFeatureEnabled(FeatureFlag.DYNAMIC_PLUGINS)) {
fetchAll();
}
}, []);

return (
<PluginContext.Provider value={pluginState}>
{children}
</PluginContext.Provider>
);
}
41 changes: 41 additions & 0 deletions superset-frontend/src/components/DynamicPlugins/PluginContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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, { useContext } from 'react';

export type PluginContextType = {
loading: boolean;
plugins: {
[key: string]: {
key: string;
loading: boolean;
error: null | Error;
};
};
fetchAll: () => void;
};

export const dummyPluginContext: PluginContextType = {
loading: true,
plugins: {},
fetchAll: () => {},
};

export const PluginContext = React.createContext(dummyPluginContext);

export const useDynamicPluginContext = () => useContext(PluginContext);
5 changes: 4 additions & 1 deletion superset-frontend/src/dashboard/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';

import DynamicPluginProvider from 'src/components/DynamicPlugins/DynamicPluginProvider';
import setupApp from '../setup/setupApp';
import setupPlugins from '../setup/setupPlugins';
import DashboardContainer from './containers/Dashboard';
Expand All @@ -31,7 +32,9 @@ setupPlugins();
const App = ({ store }) => (
<Provider store={store}>
<ThemeProvider theme={supersetTheme}>
<DashboardContainer />
<DynamicPluginProvider>
<DashboardContainer />
</DynamicPluginProvider>
</ThemeProvider>
</Provider>
);
Expand Down
7 changes: 6 additions & 1 deletion superset-frontend/src/dashboard/components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@superset-ui/core';

import { PluginContext } from 'src/components/DynamicPlugins/PluginContext';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
import DashboardBuilder from '../containers/DashboardBuilder';
Expand Down Expand Up @@ -68,7 +69,8 @@ const defaultProps = {
};

class Dashboard extends React.PureComponent {
// eslint-disable-next-line react/sort-comp
static contextType = PluginContext;

static onBeforeUnload(hasChanged) {
if (hasChanged) {
window.addEventListener('beforeunload', Dashboard.unload);
Expand Down Expand Up @@ -241,6 +243,9 @@ class Dashboard extends React.PureComponent {
}

render() {
if (this.context.loading) {
return 'loading...';
}
return (
<>
<OmniContainer logEvent={this.props.actions.logEvent} />
Expand Down
5 changes: 3 additions & 2 deletions superset-frontend/src/explore/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/core';
import DynamicPluginProvider from 'src/components/DynamicPlugins/DynamicPluginProvider';
import ToastPresenter from '../messageToasts/containers/ToastPresenter';
import ExploreViewContainer from './components/ExploreViewContainer';
import setupApp from '../setup/setupApp';
Expand All @@ -33,10 +34,10 @@ setupPlugins();
const App = ({ store }) => (
<Provider store={store}>
<ThemeProvider theme={supersetTheme}>
<>
<DynamicPluginProvider>
<ExploreViewContainer />
<ToastPresenter />
</>
</DynamicPluginProvider>
</ThemeProvider>
</Provider>
);
Expand Down
Loading