blob: 52570fee0391f47fc9af99bdb23a7da47acb2d4f [file] [log] [blame]
/**
* 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, useEffect, useReducer } from 'react';
import { defineSharedModules, logging, makeApi } from '@superset-ui/core';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
export type PluginContextType = {
loading: boolean;
plugins: {
[key: string]: {
key: string;
loading: boolean;
error: null | Error;
};
};
fetchAll: () => void;
};
const dummyPluginContext: PluginContextType = {
loading: true,
plugins: {},
fetchAll: () => {},
};
/**
* It is highly recommended to use the useDynamicPluginContext hook instead.
* @see useDynamicPluginContext
*/
export const PluginContext = React.createContext(dummyPluginContext);
/**
* The plugin context provides info about what dynamic plugins are available.
* It also provides loading info for the plugins' javascript bundles.
*
* Note: This does not include any information about static plugins.
* Those are compiled into the Superset bundle at build time.
* Dynamic plugins are added by the end user and can be any webhosted javascript.
*/
export const useDynamicPluginContext = () => useContext(PluginContext);
// the plugin returned from the API
type Plugin = {
name: string;
key: string;
bundle_url: string;
id: number;
};
// when a plugin completes loading
type CompleteAction = {
type: 'complete';
key: string;
error: null | Error;
};
// when plugins start loading
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;
}
}
const pluginApi = makeApi<{}, { result: Plugin[] }>({
method: 'GET',
endpoint: '/dynamic-plugins/api/read',
});
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 const DynamicPluginProvider: React.FC = ({ children }) => {
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
});
// For now, we fetch all the plugins at the same time.
// In the future it would be nice to fetch on an as-needed basis.
// That will most likely depend on having a json manifest for each plugin.
async function fetchAll() {
try {
await defineSharedModules(sharedModules);
const { result: plugins } = await pluginApi({});
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) {
logging.error(
`Failed to load plugin ${plugin.key} with the following error:`,
err.stack,
);
error = err;
}
dispatch({
type: 'complete',
key: plugin.key,
error,
});
}),
);
} catch (error) {
logging.error('Failed to load dynamic plugins', error.stack || error);
}
}
useEffect(() => {
if (isFeatureEnabled(FeatureFlag.DYNAMIC_PLUGINS)) {
fetchAll();
}
}, []);
return (
<PluginContext.Provider value={pluginState}>
{children}
</PluginContext.Provider>
);
};