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

Conversation

suddjian
Copy link
Member

@suddjian suddjian commented Jul 10, 2020

SUMMARY

Adds functionality to import plugins dynamically. A corresponding plugin can be found here: https://github.com/apache-superset/dynamic-import-demo-plugin

Goals:

  • Allow third parties to create plugins without changing Superset code
  • Allow users of Superset to add new chart types without backend access
  • Allow plugin authors to use whatever languages and code styles they prefer
  • Work towards a future improved contract between superset and third-party code.
  • Open the door to developing a future “app store” so that plugins can be conveniently searched and installed at the press of a button

How it works:

  • Plugin author starts with the template repo with a structure similar to existing plugins. Builds a bundle using Webpack.
  • Shared packages (react, lodash, @superset-ui) are provided superset-side and are referenced as externals from Webpack.
  • Bundle is hosted somewhere on the web - CDN, github etc. The plugin has serve command to help with local development.
  • Use the new Custom Plugin UI in Superset (under the "Manage" menu) to add the plugin to Superset.

Various hacks were necessary to build a PoC, but I believe they can all be solved with some kind of arcane webpack incantation:

  • Assigning modules to namespaced global objects to support a plugin's external dependencies instead of an actual dependency management system
  • Can’t async import from inside the plugin
  • Images/other assets. How do we load them if we don't know where the plugin is going to be hosted? Currently using url-loader with limit: Infinity, which works but is kinda sad.
  • I'm still tracking down all the places in Superset that need to change to respect async import of plugins.

I'd greatly appreciate any help addressing these issues, or any feedback on the code that anyone has!

TEST PLAN

  • toggle on the feature flag
  • run the example plugin linked above
  • add the example plugin from the "Plugins" settings nav menu item.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Changes UI
  • Requires DB Migration.
  • Confirm DB Migration upgrade and downgrade tested.
  • Introduces new feature or API
  • Removes existing feature or API

@suddjian suddjian changed the title Dynamically imported viz plugins feat: Dynamically imported viz plugins Jul 11, 2020
@suddjian suddjian force-pushed the dynamic-plugin-import branch from d5ebb30 to 42f97e7 Compare August 11, 2020 16:51
@amitmiran137
Copy link
Member

@suddjian I'm happy to help out here

@suddjian
Copy link
Member Author

Thanks for the offer @amitNielsen! I won't have a chance to get back on this until next week, but if we can address the CI errors I think it should be just about ready for review. Just needs one last pass to make sure that every part of the app knows how to wait for the async plugins.

If you make some fixes I can merge your branch 🙂

@amitmiran137
Copy link
Member

@suddjian I tried solving the issue in the explore page where controlsPanel section / row is not re-rendering the exiting parameters of the query like group by and metrics.

Can we maybe do some knowledge transfer on how what exactly should be done there and maybe do some refactoring in those areas

you can also have a look at my PR that solves few of the issues already
suddjian#2

@codecov-commenter
Copy link

codecov-commenter commented Aug 28, 2020

Codecov Report

Merging #10288 into master will decrease coverage by 6.16%.
The diff coverage is 64.26%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #10288      +/-   ##
==========================================
- Coverage   64.76%   58.59%   -6.17%     
==========================================
  Files         789      762      -27     
  Lines       37091    36142     -949     
  Branches     3555     3315     -240     
==========================================
- Hits        24021    21177    -2844     
- Misses      12963    14774    +1811     
- Partials      107      191      +84     
Flag Coverage Δ
#cypress 55.52% <63.00%> (-0.10%) ⬇️
#javascript ?
#python 60.29% <71.11%> (+0.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
superset-frontend/src/chart/chartReducer.js 62.50% <0.00%> (-6.36%) ⬇️
superset-frontend/src/dashboard/App.jsx 100.00% <ø> (ø)
superset-frontend/src/explore/App.jsx 100.00% <ø> (ø)
...et-frontend/src/explore/reducers/exploreReducer.js 42.62% <0.00%> (-4.84%) ⬇️
superset-frontend/src/views/App.tsx 100.00% <ø> (+42.10%) ⬆️
...ns/versions/73fd22e742ab_add_dynamic_plugins_py.py 0.00% <0.00%> (ø)
superset/models/__init__.py 100.00% <ø> (ø)
superset/views/__init__.py 100.00% <ø> (ø)
superset/viz_sip38.py 0.00% <0.00%> (ø)
...omponents/DynamicPlugins/DynamicPluginProvider.tsx 10.41% <10.41%> (ø)
... and 300 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c715cad...2cf793f. Read the comment docs.

@amitmiran137
Copy link
Member

amitmiran137 commented Aug 29, 2020

@suddjian I've fix the pre-commit issues for re configuring 3rd party and then re running sort
for some reason I can't pull request to your repo as last time 🤔

here is my branch with the fix : https://github.com/amitnielsen/incubator-superset/tree/dynamic-plugin-import

================ update =================
I've also removed a depracted test
added Loading component instead of '...loading'

@amitmiran137
Copy link
Member

@suddjian I suggest we also write an E2E cypress test for the explore page and dashboard page that loads a dynamic plugin stable

currently with the https://github.com/apache-superset/dynamic-import-demo-plugin
I can't say for certain that plugins work 100% , currently this plugin doesn't do the initial load currently

we should also stabalize the demo plugin to be aligned with the latest yeo generated plugin, which is what all community plugins are starting from

@suddjian
Copy link
Member Author

suddjian commented Aug 31, 2020

A cypress test sounds like a good idea to me.

I have the example plugin working locally, maybe I can help there?

I think dynamic plugins in the long run will have a different set of requirements than static plugins. I'm thinking about adding some kind of manifest json file to specify the plugin name, key, thumbnail, version, and main module. That would allow the backend to inspect plugin info without loading the plugin in a js environment. We could build these things into the yeoman generator, but I think for now this should be considered a very experimental feature and subject to change.

Copy link
Member

@ktmud ktmud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the awesome work! I think exposing @superset-ui packages as globals is the right way to go. In the future, there will only be one package so it should become more or less more manageable. For security reasons, we want to limit what methods are exposed in the superset-ui package available to plugins, but that's another story.

superset-frontend/src/explore/App.jsx Outdated Show resolved Hide resolved
'@superset-ui/style': import('@superset-ui/style'),
'@superset-ui/translation': import('@superset-ui/translation'),
'@superset-ui/validator': import('@superset-ui/validator'),
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should defineSharedModules be called outside of fetchAll or in a separate setup function? It would also be nice to make DynamicProviderProvider load only specific plugins on demand instead of all together.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to do loading on demand, but the way I got the example plugin's webpack to work is it references global variables on the window. I intend to define a superset-plugin.json manifest file for dynamic plugins in the future, which would be a good place to put dependency info. For now I'd like to keep this minimal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This design is also not ideal for third-party plugins that may want to add libraries for other plugins to depend on. Definitely should be improved.

@stale
Copy link

stale bot commented Nov 26, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. For admin, please label this issue .pinned to prevent stale bot from closing the issue.

@stale stale bot added the inactive Inactive for >= 30 days label Nov 26, 2020
@lozbrown
Copy link

lozbrown commented Dec 1, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. For admin, please label this issue .pinned to prevent stale bot from closing the issue.

Really bot, no! this would be super useful

@stale stale bot removed the inactive Inactive for >= 30 days label Dec 1, 2020
@@ -167,53 +170,59 @@ export function getControlState(controlKey, vizType, state, value) {
);
}

const getMemoizedSectionsToRender = memoizeOne(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

}
});
Object.keys(controlsState)
.concat(Object.keys(inputFormData))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamically imported plugins don't have any controls state available until after the plugin has loaded. This change makes it so that we use the fields from the chart's form data instead of only using the controls.

@@ -21,26 +21,29 @@ import memoize from 'lodash/memoize';
import { getChartControlPanelRegistry } from '@superset-ui/core';
import { controls } from '../explore/controls';

const getControlsForVizType = memoize(vizType => {
const memoizedControls = memoize((vizType, controlPanel) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to change memoization again here

@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=too-few-public-methods,invalid-name
from dataclasses import dataclass
from dataclasses import dataclass # pylint: disable=wrong-import-order
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't know why but every linter decided to lose its shit here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What version of Python are you on? I'm guessing the ordering fails due to dataclasses being built-in on 3.7 but an addon in previous versions. But no need to fix here, I can clean this up later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on 3.7

@@ -122,138 +127,110 @@ const Styles = styled.div`
}
`;

class ExploreViewContainer extends React.Component {
Copy link
Member Author

@suddjian suddjian Dec 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted this to a function component out of frustration with trying to change behavior. Didn't have the stamina to go all the way and convert it to typescript.

"DatabaseAsync",
"DatabaseView",
"DruidClusterModelView",
"DynamicPlugin",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes it so that only admins can write to dynamic plugins endpoints

if (title) {
document.title = title;
useEffect(() => {
if (wasDynamicPluginLoading && !isDynamicPluginLoading) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this effect is the only functionality in this file that's actually new

@suddjian suddjian force-pushed the dynamic-plugin-import branch from 521dccf to c0e0847 Compare December 19, 2020 06:38
@suddjian suddjian force-pushed the dynamic-plugin-import branch from c0e0847 to 7e637ab Compare December 19, 2020 06:39
Copy link
Member

@pkdotson pkdotson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Super excited to see this within superset!

Copy link
Member

@villebro villebro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Comment on lines +42 to +47
// jest.spyOn(ReactAll, 'useContext').mockImplementation(() => {
// return {
// store,
// subscription: new Subscription(store),
// };
// });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I prefer /* */ for multi-line comments

@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=too-few-public-methods,invalid-name
from dataclasses import dataclass
from dataclasses import dataclass # pylint: disable=wrong-import-order
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What version of Python are you on? I'm guessing the ordering fails due to dataclasses being built-in on 3.7 but an addon in previous versions. But no need to fix here, I can clean this up later.

@junlincc junlincc merged commit b5dd0f3 into apache:master Dec 19, 2020
@@ -107,11 +113,12 @@ describe('chart card view', () => {
cy.get('[data-test="modal-cancel-button"]').click();
});

it('should edit correctly', () => {
// flaky
xit('should edit correctly', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we nix so many tests? Were they causing problems in other PRs, too? If yes, I think it would be better if they were turned off in a separate PR so we can keep better track of them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these tests failed and passed at various times, as well as on other PRs. We did a little investigation but haven't found the cause of flakiness yet.

@betodealmeida betodealmeida added the risk:db-migration PRs that require a DB migration label Dec 22, 2020
@betodealmeida
Copy link
Member

Migration is failing on MySQL with:

sqlalchemy.exc.OperationalError: (MySQLdb._exceptions.OperationalError) (1071, 'Specified key was too long; max key length is 3072 bytes')

@suddjian
Copy link
Member Author

@betodealmeida That's very strange... Any idea why? It passed CI.

@villebro
Copy link
Member

@betodealmeida what version of MySQL are you seeing this on?

@dpgaspar
Copy link
Member

https://stackoverflow.com/questions/8746207/1071-specified-key-was-too-long-max-key-length-is-1000-bytes

Indicates that it could be related to the charset or a non InnoDB engine. Maybe?

@mistercrunch mistercrunch added 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 1.0.0 labels Feb 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels risk:db-migration PRs that require a DB migration size/XXL 🚢 1.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.