From 2cdd63529abc3a4376bbff65c70a09ed97ffbe6d Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 22 Jan 2024 11:25:25 -0500 Subject: [PATCH 01/17] WIP dynamic callbacks --- .../src/components/StyledComponent.js | 4 +- dash/_dash_renderer.py | 4 +- dash/dash-renderer/src/APIController.react.js | 33 +-- .../src/CheckedComponent.react.js | 30 +++ dash/dash-renderer/src/TreeContainer.js | 69 ++---- .../src/libraries/LibraryComponent.tsx | 47 ++++ .../src/libraries/LibraryManager.tsx | 21 ++ .../src/libraries/createLibraryElement.js | 10 + .../src/libraries/librariesContext.ts | 229 ++++++++++++++++++ dash/dash-renderer/src/persistence.js | 9 +- dash/dash.py | 60 ++++- dash/development/base_component.py | 4 +- dash/resources.py | 12 + 13 files changed, 441 insertions(+), 91 deletions(-) create mode 100644 dash/dash-renderer/src/CheckedComponent.react.js create mode 100644 dash/dash-renderer/src/libraries/LibraryComponent.tsx create mode 100644 dash/dash-renderer/src/libraries/LibraryManager.tsx create mode 100644 dash/dash-renderer/src/libraries/createLibraryElement.js create mode 100644 dash/dash-renderer/src/libraries/librariesContext.ts diff --git a/@plotly/dash-test-components/src/components/StyledComponent.js b/@plotly/dash-test-components/src/components/StyledComponent.js index f7e8703f40..1fc6664d9a 100644 --- a/@plotly/dash-test-components/src/components/StyledComponent.js +++ b/@plotly/dash-test-components/src/components/StyledComponent.js @@ -15,7 +15,7 @@ StyledComponent.propTypes = { /** * The style */ - style: PropTypes.shape, + style: PropTypes.any, /** * The value to display @@ -27,4 +27,4 @@ StyledComponent.defaultProps = { value: '' }; -export default StyledComponent; \ No newline at end of file +export default StyledComponent; diff --git a/dash/_dash_renderer.py b/dash/_dash_renderer.py index 75940edfe7..895984e96a 100644 --- a/dash/_dash_renderer.py +++ b/dash/_dash_renderer.py @@ -1,6 +1,6 @@ import os -__version__ = "1.17.0" +__version__ = "1.17.1" _available_react_versions = {"16.14.0", "18.2.0"} _available_reactdom_versions = {"16.14.0", "18.2.0"} @@ -64,7 +64,7 @@ def _set_react_version(v_react, v_reactdom=None): { "relative_package_path": "dash-renderer/build/dash_renderer.min.js", "dev_package_path": "dash-renderer/build/dash_renderer.dev.js", - "external_url": "https://unpkg.com/dash-renderer@1.17.0" + "external_url": "https://unpkg.com/dash-renderer@1.17.1" "/build/dash_renderer.min.js", "namespace": "dash", }, diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js index ed443bad5c..2ab9b27a37 100644 --- a/dash/dash-renderer/src/APIController.react.js +++ b/dash/dash-renderer/src/APIController.react.js @@ -21,6 +21,7 @@ import {getAppState} from './reducers/constants'; import {STATUS} from './constants/constants'; import {getLoadingState, getLoadingHash} from './utils/TreeContainer'; import wait from './utils/wait'; +import LibraryManager from './libraries/LibraryManager'; export const DashContext = createContext({}); @@ -97,20 +98,24 @@ const UnconnectedContainer = props => { content = ( - + + + ); } else { diff --git a/dash/dash-renderer/src/CheckedComponent.react.js b/dash/dash-renderer/src/CheckedComponent.react.js new file mode 100644 index 0000000000..4eeed6d524 --- /dev/null +++ b/dash/dash-renderer/src/CheckedComponent.react.js @@ -0,0 +1,30 @@ +import checkPropTypes from './checkPropTypes'; +import {propTypeErrorHandler} from './exceptions'; +import {createLibraryElement} from './libraries/createLibraryElement'; +import PropTypes from 'prop-types'; + +export function CheckedComponent(p) { + const {element, extraProps, props, children, type} = p; + + const errorMessage = checkPropTypes( + element.propTypes, + props, + 'component prop', + element + ); + if (errorMessage) { + propTypeErrorHandler(errorMessage, props, type); + } + + return createLibraryElement(element, props, extraProps, children); +} + +CheckedComponent.propTypes = { + children: PropTypes.any, + element: PropTypes.any, + layout: PropTypes.any, + props: PropTypes.any, + extraProps: PropTypes.any, + id: PropTypes.string, + type: PropTypes.string +}; diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index 19dd71872e..834305aa07 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -1,7 +1,5 @@ import React, {Component, memo, useContext} from 'react'; import PropTypes from 'prop-types'; -import Registry from './registry'; -import {propTypeErrorHandler} from './exceptions'; import { addIndex, assoc, @@ -9,25 +7,25 @@ import { concat, dissoc, equals, + has, isEmpty, isNil, - has, keys, map, mapObjIndexed, - mergeRight, + path as rpath, + pathOr, pick, pickBy, propOr, - path as rpath, - pathOr, type } from 'ramda'; +import {batch} from 'react-redux'; + import {notifyObservers, updateProps} from './actions'; import isSimpleComponent from './isSimpleComponent'; import {recordUiEdit} from './persistence'; import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react'; -import checkPropTypes from './checkPropTypes'; import {getWatchedKeys, stringifyId} from './actions/dependencies'; import { getLoadingHash, @@ -35,45 +33,12 @@ import { validateComponent } from './utils/TreeContainer'; import {DashContext} from './APIController.react'; -import {batch} from 'react-redux'; +import LibraryComponent from './libraries/LibraryComponent'; const NOT_LOADING = { is_loading: false }; -function CheckedComponent(p) { - const {element, extraProps, props, children, type} = p; - - const errorMessage = checkPropTypes( - element.propTypes, - props, - 'component prop', - element - ); - if (errorMessage) { - propTypeErrorHandler(errorMessage, props, type); - } - - return createElement(element, props, extraProps, children); -} - -CheckedComponent.propTypes = { - children: PropTypes.any, - element: PropTypes.any, - layout: PropTypes.any, - props: PropTypes.any, - extraProps: PropTypes.any, - id: PropTypes.string -}; - -function createElement(element, props, extraProps, children) { - const allProps = mergeRight(props, extraProps); - if (Array.isArray(children)) { - return React.createElement(element, allProps, ...children); - } - return React.createElement(element, allProps, children); -} - function isDryComponent(obj) { return ( type(obj) === 'Object' && @@ -240,7 +205,8 @@ class BaseTreeContainer extends Component { } validateComponent(_dashprivate_layout); - const element = Registry.resolve(_dashprivate_layout); + // console.log("Dash private config", _dashprivate_config); + // const element = Registry.resolve(_dashprivate_layout); // Hydrate components props const childrenProps = pathOr( @@ -445,17 +411,14 @@ class BaseTreeContainer extends Component { dispatch={_dashprivate_dispatch} error={_dashprivate_error} > - {_dashprivate_config.props_check ? ( - - ) : ( - createElement(element, props, extraProps, children) - )} + ); } diff --git a/dash/dash-renderer/src/libraries/LibraryComponent.tsx b/dash/dash-renderer/src/libraries/LibraryComponent.tsx new file mode 100644 index 0000000000..0b8b18104c --- /dev/null +++ b/dash/dash-renderer/src/libraries/LibraryComponent.tsx @@ -0,0 +1,47 @@ +import React, {useContext, useEffect} from 'react'; +import {LibrariesContext} from './librariesContext'; +import Registry from '../registry'; +import {CheckedComponent} from '../CheckedComponent.react'; +import {createLibraryElement} from './createLibraryElement'; + +type LibraryComponentProps = { + type: string; + namespace: string; + props: any; + extraProps: any; + children: any; + props_check: boolean; +}; + +const LibraryComponent = (props: LibraryComponentProps) => { + const {props_check, namespace, type, ...rest} = props; + + const context = useContext(LibrariesContext); + + useEffect(() => { + context.addToLoad(namespace); + }, []); + + if (!context.isLoaded(namespace)) { + return <>; + } + const element = Registry.resolve({namespace, type}); + if (props_check) { + return ( + + ); + } + return createLibraryElement( + element, + rest.props, + rest.extraProps, + rest.children + ); +}; +export default LibraryComponent; diff --git a/dash/dash-renderer/src/libraries/LibraryManager.tsx b/dash/dash-renderer/src/libraries/LibraryManager.tsx new file mode 100644 index 0000000000..24d5c1e0d9 --- /dev/null +++ b/dash/dash-renderer/src/libraries/LibraryManager.tsx @@ -0,0 +1,21 @@ +import React, {JSX} from 'react'; + +import {createLibrariesContext, LibrariesContext} from './librariesContext'; + +type LibrariesManagerProps = { + children: JSX.Element; + requests_pathname_prefix: string; +}; + +const LibraryManager = (props: LibrariesManagerProps) => { + const {children, requests_pathname_prefix} = props; + const contextValue = createLibrariesContext(requests_pathname_prefix); + + return ( + + {children} + + ); +}; + +export default LibraryManager; diff --git a/dash/dash-renderer/src/libraries/createLibraryElement.js b/dash/dash-renderer/src/libraries/createLibraryElement.js new file mode 100644 index 0000000000..e5ebe6fa68 --- /dev/null +++ b/dash/dash-renderer/src/libraries/createLibraryElement.js @@ -0,0 +1,10 @@ +import {mergeRight} from 'ramda'; +import React from 'react'; + +export function createLibraryElement(element, props, extraProps, children) { + const allProps = mergeRight(props, extraProps); + if (Array.isArray(children)) { + return React.createElement(element, allProps, ...children); + } + return React.createElement(element, allProps, children); +} diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts new file mode 100644 index 0000000000..849c118303 --- /dev/null +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -0,0 +1,229 @@ +import { + createContext, + useContext, + useReducer, + useEffect, + useState +} from 'react'; +import {assocPath, pathOr, pipe, toPairs} from 'ramda'; + +type LibraryResource = { + type: '_js_dist' | '_css_dist'; + url: string; + async?: string; + namespace: string; + relative_package_path?: string; +}; + +type LibrariesState = { + [libname: string]: { + toLoad: boolean; + loading: boolean; + loaded: boolean; + dist?: LibraryResource[]; + }; +}; + +enum LibrariesActions { + LOAD, + LOADED, + TO_LOAD +} + +type LoadingPayload = { + libraries: string[]; +}; + +type LoadedPayload = { + libraries: string[]; +}; + +type ToLoadPayload = { + library: string; +}; + +type LibrariesAction = { + type: LibrariesActions; + payload: LoadingPayload | LoadedPayload | ToLoadPayload; +}; + +export type LibrariesContextType = { + state: LibrariesState; + setLoading: (payload: LoadingPayload) => void; + setLoaded: (payload: LoadedPayload) => void; + setToLoad: (payload: ToLoadPayload) => void; + isLoading: (libraryName: string) => boolean; + isLoaded: (libraryName: string) => boolean; + fetchLibraries: () => void; + getLibrariesToLoad: () => string[]; + addToLoad: (libName: string) => void; +}; + +function handleLoad(library: string, state: LibrariesState) { + return pipe( + assocPath([library, 'loading'], true), + assocPath([library, 'toLoad'], false) + )(state) as LibrariesState; +} + +function handleLoaded(library: string, state: LibrariesState) { + return pipe( + assocPath([library, 'loaded'], true), + assocPath([library, 'loading'], false) + )(state) as LibrariesState; +} + +export function librariesReducer( + state: LibrariesState, + action: LibrariesAction +): LibrariesState { + switch (action.type) { + case LibrariesActions.LOAD: + return (action.payload as LoadingPayload).libraries.reduce( + (acc, lib) => handleLoad(lib, acc), + state + ); + case LibrariesActions.LOADED: + return (action.payload as LoadedPayload).libraries.reduce( + (acc, lib) => handleLoaded(lib, acc), + state + ); + case LibrariesActions.TO_LOAD: + return pipe( + assocPath( + [(action.payload as ToLoadPayload).library, 'toLoad'], + true + ) + )(state) as LibrariesState; + default: + return state; + } +} + +export function createLibrariesContext( + pathnamePrefix: string +): LibrariesContextType { + const [state, dispatch] = useReducer(librariesReducer, {}); + const [callback, setCallback] = useState(-1); + const createAction = (type: LibrariesActions) => (payload: any) => + dispatch({type, payload}); + + const setLoading = createAction(LibrariesActions.LOAD); + const setLoaded = createAction(LibrariesActions.LOADED); + const setToLoad = createAction(LibrariesActions.TO_LOAD); + + const isLoaded = (libraryName: string) => + pathOr(false, [libraryName, 'loaded'], state); + const isLoading = (libraryName: string) => + pathOr(false, [libraryName, 'loading'], state); + + const addToLoad = (libraryName: string) => { + const lib = state[libraryName]; + if (!lib) { + // Check if already loaded on the window + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (window[libraryName]) { + setLoaded({libraries: [libraryName]}); + } else { + setToLoad({library: libraryName}); + } + } + // if lib is already in don't do anything. + }; + + const getLibrariesToLoad = () => + toPairs(state).reduce((acc: string[], [key, value]) => { + if (value.toLoad) { + acc.push(key); + } + return acc; + }, []); + + const fetchLibraries = () => { + const libraries = getLibrariesToLoad(); + if (!libraries.length) { + return; + } + + setLoading({libraries}); + + fetch(`${pathnamePrefix}_dash-dist`, { + body: JSON.stringify(libraries), + headers: {'Content-Type': 'application/json'}, + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + const head = document.querySelector('head'); + const loadPromises: Promise[] = []; + data.forEach((resource: LibraryResource) => { + if (resource.type === '_js_dist') { + const element = document.createElement('script'); + element.src = resource.url; + element.async = true; + loadPromises.push( + new Promise((resolve, reject) => { + element.onload = () => { + resolve(); + }; + element.onerror = error => reject(error); + }) + ); + head?.appendChild(element); + } else if (resource.type === '_css_dist') { + const element = document.createElement('link'); + element.href = resource.url; + element.rel = 'stylesheet'; + loadPromises.push( + new Promise((resolve, reject) => { + element.onload = () => { + resolve(); + }; + element.onerror = error => reject(error); + }) + ); + head?.appendChild(element); + } + }); + return Promise.all(loadPromises); + }) + .then(() => { + setLoaded({libraries}); + setCallback(-1); + }); + }; + + // Load libraries on a throttle to have time to gather all the components in one go. + useEffect(() => { + const libraries = getLibrariesToLoad(); + if (!libraries.length) { + return; + } + if (callback > 0) { + window.clearTimeout(callback); + } + const timeout = window.setTimeout(fetchLibraries, 0); + setCallback(timeout); + }, [state]); + + return { + state, + setLoading, + setLoaded, + setToLoad, + isLoaded, + isLoading, + fetchLibraries, + getLibrariesToLoad, + addToLoad + }; +} + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export const LibrariesContext = createContext(null); + +export function useDashLibraries() { + return useContext(LibrariesContext); +} diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index b7bc2719ba..34a6c38158 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -68,7 +68,7 @@ import { } from 'ramda'; import {createAction} from 'redux-actions'; -import Registry from './registry'; +// import Registry from './registry'; import {stringifyId} from './actions/dependencies'; export const storePrefix = '_dash_persistence.'; @@ -289,8 +289,7 @@ const getProps = layout => { } const {id, persistence} = props; - const element = Registry.resolve(layout); - const getVal = prop => props[prop] || (element.defaultProps || {})[prop]; + const getVal = prop => props[prop] || {}[prop]; const persisted_props = getVal('persisted_props'); const persistence_type = getVal('persistence_type'); const canPersist = id && persisted_props && persistence_type; @@ -299,7 +298,6 @@ const getProps = layout => { canPersist, id, props, - element, persistence, persisted_props, persistence_type @@ -311,7 +309,6 @@ export function recordUiEdit(layout, newProps, dispatch) { canPersist, id, props, - element, persistence, persisted_props, persistence_type @@ -324,7 +321,7 @@ export function recordUiEdit(layout, newProps, dispatch) { const [propName, propPart] = persistedProp.split('.'); if (newProps[propName] !== undefined) { const storage = getStore(persistence_type, dispatch); - const {extract} = getTransform(element, propName, propPart); + const {extract} = getTransform({}, propName, propPart); const valsKey = getValsKey(id, persistedProp, persistence); let originalVal = extract(props[propName]); diff --git a/dash/dash.py b/dash/dash.py index 08b5ea34c3..2812569238 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -392,6 +392,8 @@ def __init__( # pylint: disable=too-many-statements add_log_handler=True, hooks: Union[RendererHooks, None] = None, routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None, + dynamic_loading=True, + preloaded_libraries=None, **obsolete, ): _validate.check_obsolete(obsolete) @@ -446,6 +448,8 @@ def __init__( # pylint: disable=too-many-statements title=title, update_title=update_title, include_pages_meta=include_pages_meta, + dynamic_loading=dynamic_loading, + preloaded_libraries=preloaded_libraries or [], ) self.config.set_read_only( [ @@ -456,6 +460,8 @@ def __init__( # pylint: disable=too-many-statements "serve_locally", "compress", "pages_folder", + "dynamic_loading", + "preloaded_libraries", ], "Read-only: can only be set in the Dash constructor", ) @@ -633,6 +639,7 @@ def _setup_routes(self): self._add_url("_dash-update-component", self.dispatch, ["POST"]) self._add_url("_reload-hash", self.serve_reload_hash) self._add_url("_favicon.ico", self._serve_default_favicon) + self._add_url("_dash-dist", self.serve_dist, methods=["POST"]) self._add_url("", self.index) if jupyter_dash.active: @@ -786,6 +793,20 @@ def serve_reload_hash(self): } ) + def serve_dist(self): + libraries = flask.request.get_json() + dists = [] + for dist_type in ("_js_dist", "_css_dist"): + resources = [ + resource + for resource in ComponentRegistry.get_resources(dist_type, libraries) + if not resource.get("async") and not resource.get("dynamic") + ] + srcs = self._collect_and_register_resources(resources) + for src in srcs: + dists.append(dict(type=dist_type, url=src)) + return flask.jsonify(dists) + def _collect_and_register_resources(self, resources): # now needs the app context. # template in the necessary component suite JS bundles @@ -850,7 +871,13 @@ def _relative_url_path(relative_package_path="", namespace=""): def _generate_css_dist_html(self): external_links = self.config.external_stylesheets - links = self._collect_and_register_resources(self.css.get_all_css()) + + if self.config.dynamic_loading: + links = self._collect_and_register_resources( + self.css.get_library_css(self.config.preloaded_libraries) + ) + else: + links = self._collect_and_register_resources(self.css.get_all_css()) return "\n".join( [ @@ -885,21 +912,28 @@ def _generate_scripts_html(self): self.scripts._resources._filter_resources(deps, dev_bundles=dev) ) + self.config.external_scripts - + self._collect_and_register_resources( + ) + + if not self.config.dynamic_loading: + srcs += self._collect_and_register_resources( self.scripts.get_all_scripts(dev_bundles=dev) - + self.scripts._resources._filter_resources( - _dash_renderer._js_dist, dev_bundles=dev - ) - + self.scripts._resources._filter_resources( - dcc._js_dist, dev_bundles=dev - ) - + self.scripts._resources._filter_resources( - html._js_dist, dev_bundles=dev - ) - + self.scripts._resources._filter_resources( - dash_table._js_dist, dev_bundles=dev + ) + else: + srcs += self._collect_and_register_resources( + self.scripts.get_library_scripts( + self.config.preloaded_libraries, dev_bundles=dev ) ) + + srcs += self._collect_and_register_resources( + self.scripts._resources._filter_resources( + _dash_renderer._js_dist, dev_bundles=dev + ) + + self.scripts._resources._filter_resources(dcc._js_dist, dev_bundles=dev) + + self.scripts._resources._filter_resources(html._js_dist, dev_bundles=dev) + + self.scripts._resources._filter_resources( + dash_table._js_dist, dev_bundles=dev + ) ) self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index b51fc06634..0bc4a28c9c 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -20,10 +20,12 @@ class ComponentRegistry: children_props = collections.defaultdict(dict) @classmethod - def get_resources(cls, resource_name): + def get_resources(cls, resource_name, includes=None): resources = [] for module_name in cls.registry: + if includes is not None and module_name not in includes: + continue module = sys.modules[module_name] resources.extend(getattr(module, resource_name, [])) diff --git a/dash/resources.py b/dash/resources.py index cae4a49470..2ac080d57a 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -88,6 +88,12 @@ def get_all_resources(self, dev_bundles=False): return self._filter_resources(all_resources, dev_bundles) + def get_library_resources(self, libraries, dev_bundles=False): + lib_resources = ComponentRegistry.get_resources(self.resource_name, libraries) + all_resources = lib_resources + self._resources + + return self._filter_resources(all_resources, dev_bundles) + # pylint: disable=too-few-public-methods class _Config: @@ -107,6 +113,9 @@ def append_css(self, stylesheet): def get_all_css(self): return self._resources.get_all_resources() + def get_library_css(self, libraries): + return self._resources.get_library_resources(libraries) + class Scripts: def __init__(self, serve_locally, eager): @@ -118,3 +127,6 @@ def append_script(self, script): def get_all_scripts(self, dev_bundles=False): return self._resources.get_all_resources(dev_bundles) + + def get_library_scripts(self, libraries, dev_bundles=False): + return self._resources.get_library_resources(libraries, dev_bundles) From 008cb60357102d22955912014db61a6ec1ecbf7f Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 7 Feb 2024 09:44:03 -0500 Subject: [PATCH 02/17] Fix initial loading of libraries. --- dash/dash-renderer/src/APIController.react.js | 75 ++++++++++++------- .../src/libraries/LibraryManager.tsx | 56 ++++++++++++-- .../src/libraries/librariesContext.ts | 23 +++++- dash/dash-renderer/src/persistence.js | 9 ++- 4 files changed, 125 insertions(+), 38 deletions(-) diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js index 2ab9b27a37..9e90dc6268 100644 --- a/dash/dash-renderer/src/APIController.react.js +++ b/dash/dash-renderer/src/APIController.react.js @@ -1,6 +1,12 @@ import {batch, connect} from 'react-redux'; import {includes, isEmpty} from 'ramda'; -import React, {useEffect, useRef, useState, createContext} from 'react'; +import React, { + useEffect, + useRef, + useState, + createContext, + useCallback +} from 'react'; import PropTypes from 'prop-types'; import TreeContainer from './TreeContainer'; import GlobalErrorContainer from './components/error/GlobalErrorContainer.react'; @@ -47,6 +53,10 @@ const UnconnectedContainer = props => { if (!events.current) { events.current = new EventEmitter(); } + + const [libraryReady, setLibraryReady] = useState(false); + const onLibraryReady = useCallback(() => setLibraryReady(true), []); + const renderedTree = useRef(false); const propsRef = useRef({}); @@ -61,7 +71,9 @@ const UnconnectedContainer = props => { }) }); - useEffect(storeEffect.bind(null, props, events, setErrorLoading)); + useEffect( + storeEffect.bind(null, props, events, setErrorLoading, libraryReady) + ); useEffect(() => { if (renderedTree.current) { @@ -98,38 +110,43 @@ const UnconnectedContainer = props => { content = ( - - - + ); } else { content =
Loading...
; } - return config && config.ui === true ? ( - {content} - ) : ( - content + return ( + + {config && config.ui === true ? ( + {content} + ) : ( + content + )} + ); }; -function storeEffect(props, events, setErrorLoading) { +function storeEffect(props, events, setErrorLoading, libraryReady) { const { appLifecycle, dependenciesRequest, @@ -148,7 +165,7 @@ function storeEffect(props, events, setErrorLoading) { } dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest')); } else if (layoutRequest.status === STATUS.OK) { - if (isEmpty(layout)) { + if (isEmpty(layout) && libraryReady) { if (typeof hooks.layout_post === 'function') { hooks.layout_post(layoutRequest.content); } @@ -191,7 +208,8 @@ function storeEffect(props, events, setErrorLoading) { layoutRequest.status === STATUS.OK && !isEmpty(layout) && // Hasn't already hydrated - appLifecycle === getAppState('STARTED') + appLifecycle === getAppState('STARTED') && + libraryReady ) { let hasError = false; try { @@ -240,7 +258,8 @@ const Container = connect( graphs: state.graphs, history: state.history, error: state.error, - config: state.config + config: state.config, + paths: state.paths }), dispatch => ({dispatch}) )(UnconnectedContainer); diff --git a/dash/dash-renderer/src/libraries/LibraryManager.tsx b/dash/dash-renderer/src/libraries/LibraryManager.tsx index 24d5c1e0d9..ad248be751 100644 --- a/dash/dash-renderer/src/libraries/LibraryManager.tsx +++ b/dash/dash-renderer/src/libraries/LibraryManager.tsx @@ -1,16 +1,32 @@ -import React, {JSX} from 'react'; +import React, {JSX, useEffect, useState} from 'react'; import {createLibrariesContext, LibrariesContext} from './librariesContext'; +import {crawlLayout} from '../actions/utils'; +import {isEmpty} from 'ramda'; type LibrariesManagerProps = { children: JSX.Element; requests_pathname_prefix: string; + onReady: () => void; + ready: boolean; + layout?: any; + initialLibraries?: string[]; }; -const LibraryManager = (props: LibrariesManagerProps) => { - const {children, requests_pathname_prefix} = props; - const contextValue = createLibrariesContext(requests_pathname_prefix); - +const LibraryProvider = (props: LibrariesManagerProps) => { + const { + children, + requests_pathname_prefix, + onReady, + ready, + initialLibraries + } = props; + const contextValue = createLibrariesContext( + requests_pathname_prefix, + initialLibraries as string[], + onReady, + ready + ); return ( {children} @@ -18,4 +34,34 @@ const LibraryManager = (props: LibrariesManagerProps) => { ); }; +const LibraryManager = (props: LibrariesManagerProps) => { + const {children, ready, layout} = props; + + const [initialLibraries, setInitialLibraries] = useState( + null + ); + + useEffect(() => { + if (layout && !isEmpty(layout) && !ready && !initialLibraries) { + const libraries: string[] = []; + crawlLayout(layout, (child: any) => { + if (child.namespace && !libraries.includes(child.namespace)) { + libraries.push(child.namespace); + } + }); + setInitialLibraries(libraries); + } + }, [layout, ready, initialLibraries]); + + if (!initialLibraries) { + return children; + } + + return ( + + {children} + + ); +}; + export default LibraryManager; diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts index 849c118303..11b881de9a 100644 --- a/dash/dash-renderer/src/libraries/librariesContext.ts +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -101,9 +101,24 @@ export function librariesReducer( } export function createLibrariesContext( - pathnamePrefix: string + pathnamePrefix: string, + initialLibraries: string[], + onReady: () => void, + ready: boolean ): LibrariesContextType { - const [state, dispatch] = useReducer(librariesReducer, {}); + const [state, dispatch] = useReducer(librariesReducer, {}, () => { + const libState: LibrariesState = {}; + initialLibraries.forEach(lib => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (window[lib]) { + libState[lib] = {toLoad: false, loaded: true, loading: false}; + } else { + libState[lib] = {toLoad: true, loaded: false, loading: false}; + } + }); + return libState; + }); const [callback, setCallback] = useState(-1); const createAction = (type: LibrariesActions) => (payload: any) => dispatch({type, payload}); @@ -191,6 +206,7 @@ export function createLibrariesContext( .then(() => { setLoaded({libraries}); setCallback(-1); + onReady(); }); }; @@ -198,6 +214,9 @@ export function createLibrariesContext( useEffect(() => { const libraries = getLibrariesToLoad(); if (!libraries.length) { + if (!ready && initialLibraries.length === 0) { + onReady(); + } return; } if (callback > 0) { diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index 34a6c38158..b7bc2719ba 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -68,7 +68,7 @@ import { } from 'ramda'; import {createAction} from 'redux-actions'; -// import Registry from './registry'; +import Registry from './registry'; import {stringifyId} from './actions/dependencies'; export const storePrefix = '_dash_persistence.'; @@ -289,7 +289,8 @@ const getProps = layout => { } const {id, persistence} = props; - const getVal = prop => props[prop] || {}[prop]; + const element = Registry.resolve(layout); + const getVal = prop => props[prop] || (element.defaultProps || {})[prop]; const persisted_props = getVal('persisted_props'); const persistence_type = getVal('persistence_type'); const canPersist = id && persisted_props && persistence_type; @@ -298,6 +299,7 @@ const getProps = layout => { canPersist, id, props, + element, persistence, persisted_props, persistence_type @@ -309,6 +311,7 @@ export function recordUiEdit(layout, newProps, dispatch) { canPersist, id, props, + element, persistence, persisted_props, persistence_type @@ -321,7 +324,7 @@ export function recordUiEdit(layout, newProps, dispatch) { const [propName, propPart] = persistedProp.split('.'); if (newProps[propName] !== undefined) { const storage = getStore(persistence_type, dispatch); - const {extract} = getTransform({}, propName, propPart); + const {extract} = getTransform(element, propName, propPart); const valsKey = getValsKey(id, persistedProp, persistence); let originalVal = extract(props[propName]); From 6b74463baf26b3ac8470022c33c10e58383a97ea Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 13 Feb 2024 16:08:42 -0500 Subject: [PATCH 03/17] Fix libraries loading in callbacks. --- dash/dash-renderer/src/actions/callbacks.ts | 39 +++- dash/dash-renderer/src/actions/libraries.ts | 10 + dash/dash-renderer/src/libraries/fetchDist.ts | 12 ++ .../src/libraries/librariesContext.ts | 196 ++++-------------- .../src/libraries/libraryTypes.ts | 17 ++ .../src/libraries/loadLibrary.ts | 31 +++ dash/dash-renderer/src/reducers/libraries.ts | 60 ++++++ dash/dash-renderer/src/reducers/reducer.js | 2 + dash/dash-renderer/src/types/callbacks.ts | 3 + 9 files changed, 213 insertions(+), 157 deletions(-) create mode 100644 dash/dash-renderer/src/actions/libraries.ts create mode 100644 dash/dash-renderer/src/libraries/fetchDist.ts create mode 100644 dash/dash-renderer/src/libraries/libraryTypes.ts create mode 100644 dash/dash-renderer/src/libraries/loadLibrary.ts create mode 100644 dash/dash-renderer/src/reducers/libraries.ts diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 3390869dee..134959e69a 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -34,7 +34,7 @@ import { CallbackResponseData } from '../types/callbacks'; import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies'; -import {urlBase} from './utils'; +import {crawlLayout, urlBase} from './utils'; import {getCSRFHeader} from '.'; import {createAction, Action} from 'redux-actions'; import {addHttpHeaders} from '../actions'; @@ -44,6 +44,9 @@ import {handlePatch, isPatch} from './patch'; import {getPath} from './paths'; import {requestDependencies} from './requestDependencies'; +import loadLibrary from '../libraries/loadLibrary'; +import fetchDist from '../libraries/fetchDist'; +import {setLibraryLoaded} from './libraries'; export const addBlockedCallbacks = createAction( CallbackActionType.AddBlocked @@ -362,6 +365,7 @@ function handleServerside( let runningOff: any; let progressDefault: any; let moreArgs = additionalArgs; + const libraries = Object.keys(getState().libraries); const fetchCallback = () => { const headers = getCSRFHeader() as any; @@ -508,8 +512,37 @@ function handleServerside( } if (!long || data.response !== undefined) { - completeJob(); - finishLine(data); + const newLibs: string[] = []; + Object.values(data.response as any).forEach( + (newData: any) => { + Object.values(newData).forEach(newProp => { + crawlLayout(newProp, (c: any) => { + if (!libraries.includes(c.namespace)) { + newLibs.push(c.namespace); + } + }); + }); + } + ); + if (newLibs.length) { + fetchDist( + getState().config.requests_pathname_prefix, + newLibs + ) + .then(data => { + return Promise.all(data.map(loadLibrary)); + }) + .then(() => { + completeJob(); + finishLine(data); + dispatch( + setLibraryLoaded({libraries: newLibs}) + ); + }); + } else { + completeJob(); + finishLine(data); + } } else { // Poll chain. setTimeout( diff --git a/dash/dash-renderer/src/actions/libraries.ts b/dash/dash-renderer/src/actions/libraries.ts new file mode 100644 index 0000000000..5cac6b3597 --- /dev/null +++ b/dash/dash-renderer/src/actions/libraries.ts @@ -0,0 +1,10 @@ +import {LibrariesActions} from '../libraries/libraryTypes'; + +const createAction = (type: LibrariesActions) => (payload: any) => ({ + type, + payload +}); + +export const setLibraryLoading = createAction(LibrariesActions.LOAD); +export const setLibraryLoaded = createAction(LibrariesActions.LOADED); +export const setLibraryToLoad = createAction(LibrariesActions.TO_LOAD); diff --git a/dash/dash-renderer/src/libraries/fetchDist.ts b/dash/dash-renderer/src/libraries/fetchDist.ts new file mode 100644 index 0000000000..d82841e6d2 --- /dev/null +++ b/dash/dash-renderer/src/libraries/fetchDist.ts @@ -0,0 +1,12 @@ +import {LibraryResource} from './libraryTypes'; + +export default function fetchDist( + pathnamePrefix: string, + libraries: string[] +): Promise { + return fetch(`${pathnamePrefix}_dash-dist`, { + body: JSON.stringify(libraries), + headers: {'Content-Type': 'application/json'}, + method: 'POST' + }).then(response => response.json()); +} diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts index 11b881de9a..9fe6f62f26 100644 --- a/dash/dash-renderer/src/libraries/librariesContext.ts +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -1,57 +1,17 @@ +import {createContext, useContext, useEffect, useState} from 'react'; +import {pathOr, toPairs} from 'ramda'; +import loadLibrary from './loadLibrary'; +import {batch, useDispatch, useSelector} from 'react-redux'; +import {LibrariesState} from './libraryTypes'; import { - createContext, - useContext, - useReducer, - useEffect, - useState -} from 'react'; -import {assocPath, pathOr, pipe, toPairs} from 'ramda'; - -type LibraryResource = { - type: '_js_dist' | '_css_dist'; - url: string; - async?: string; - namespace: string; - relative_package_path?: string; -}; - -type LibrariesState = { - [libname: string]: { - toLoad: boolean; - loading: boolean; - loaded: boolean; - dist?: LibraryResource[]; - }; -}; - -enum LibrariesActions { - LOAD, - LOADED, - TO_LOAD -} - -type LoadingPayload = { - libraries: string[]; -}; - -type LoadedPayload = { - libraries: string[]; -}; - -type ToLoadPayload = { - library: string; -}; - -type LibrariesAction = { - type: LibrariesActions; - payload: LoadingPayload | LoadedPayload | ToLoadPayload; -}; + setLibraryLoaded, + setLibraryLoading, + setLibraryToLoad +} from '../actions/libraries'; +import fetchDist from './fetchDist'; export type LibrariesContextType = { state: LibrariesState; - setLoading: (payload: LoadingPayload) => void; - setLoaded: (payload: LoadedPayload) => void; - setToLoad: (payload: ToLoadPayload) => void; isLoading: (libraryName: string) => boolean; isLoaded: (libraryName: string) => boolean; fetchLibraries: () => void; @@ -59,45 +19,8 @@ export type LibrariesContextType = { addToLoad: (libName: string) => void; }; -function handleLoad(library: string, state: LibrariesState) { - return pipe( - assocPath([library, 'loading'], true), - assocPath([library, 'toLoad'], false) - )(state) as LibrariesState; -} - -function handleLoaded(library: string, state: LibrariesState) { - return pipe( - assocPath([library, 'loaded'], true), - assocPath([library, 'loading'], false) - )(state) as LibrariesState; -} - -export function librariesReducer( - state: LibrariesState, - action: LibrariesAction -): LibrariesState { - switch (action.type) { - case LibrariesActions.LOAD: - return (action.payload as LoadingPayload).libraries.reduce( - (acc, lib) => handleLoad(lib, acc), - state - ); - case LibrariesActions.LOADED: - return (action.payload as LoadedPayload).libraries.reduce( - (acc, lib) => handleLoaded(lib, acc), - state - ); - case LibrariesActions.TO_LOAD: - return pipe( - assocPath( - [(action.payload as ToLoadPayload).library, 'toLoad'], - true - ) - )(state) as LibrariesState; - default: - return state; - } +function librarySelector(s: any) { + return s.libraries as LibrariesState; } export function createLibrariesContext( @@ -106,26 +29,9 @@ export function createLibrariesContext( onReady: () => void, ready: boolean ): LibrariesContextType { - const [state, dispatch] = useReducer(librariesReducer, {}, () => { - const libState: LibrariesState = {}; - initialLibraries.forEach(lib => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (window[lib]) { - libState[lib] = {toLoad: false, loaded: true, loading: false}; - } else { - libState[lib] = {toLoad: true, loaded: false, loading: false}; - } - }); - return libState; - }); + const dispatch = useDispatch(); + const state = useSelector(librarySelector); const [callback, setCallback] = useState(-1); - const createAction = (type: LibrariesActions) => (payload: any) => - dispatch({type, payload}); - - const setLoading = createAction(LibrariesActions.LOAD); - const setLoaded = createAction(LibrariesActions.LOADED); - const setToLoad = createAction(LibrariesActions.TO_LOAD); const isLoaded = (libraryName: string) => pathOr(false, [libraryName, 'loaded'], state); @@ -139,9 +45,9 @@ export function createLibrariesContext( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (window[libraryName]) { - setLoaded({libraries: [libraryName]}); + dispatch(setLibraryLoaded({libraries: [libraryName]})); } else { - setToLoad({library: libraryName}); + dispatch(setLibraryToLoad({library: libraryName})); } } // if lib is already in don't do anything. @@ -161,57 +67,42 @@ export function createLibrariesContext( return; } - setLoading({libraries}); + dispatch(setLibraryLoading({libraries})); - fetch(`${pathnamePrefix}_dash-dist`, { - body: JSON.stringify(libraries), - headers: {'Content-Type': 'application/json'}, - method: 'POST' - }) - .then(response => response.json()) + fetchDist(pathnamePrefix, libraries) .then(data => { - const head = document.querySelector('head'); - const loadPromises: Promise[] = []; - data.forEach((resource: LibraryResource) => { - if (resource.type === '_js_dist') { - const element = document.createElement('script'); - element.src = resource.url; - element.async = true; - loadPromises.push( - new Promise((resolve, reject) => { - element.onload = () => { - resolve(); - }; - element.onerror = error => reject(error); - }) - ); - head?.appendChild(element); - } else if (resource.type === '_css_dist') { - const element = document.createElement('link'); - element.href = resource.url; - element.rel = 'stylesheet'; - loadPromises.push( - new Promise((resolve, reject) => { - element.onload = () => { - resolve(); - }; - element.onerror = error => reject(error); - }) - ); - head?.appendChild(element); - } - }); - return Promise.all(loadPromises); + return Promise.all(data.map(loadLibrary)); }) .then(() => { - setLoaded({libraries}); + dispatch(setLibraryLoaded({libraries})); setCallback(-1); onReady(); }); }; + useEffect(() => { + batch(() => { + const loaded: string[] = []; + initialLibraries.forEach(lib => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (window[lib]) { + loaded.push(lib); + } else { + dispatch(setLibraryToLoad({library: lib})); + } + }); + if (loaded.length) { + dispatch(setLibraryLoaded({libraries: loaded})); + } + }); + }, [initialLibraries]); + // Load libraries on a throttle to have time to gather all the components in one go. useEffect(() => { + if (ready) { + return; + } const libraries = getLibrariesToLoad(); if (!libraries.length) { if (!ready && initialLibraries.length === 0) { @@ -224,13 +115,10 @@ export function createLibrariesContext( } const timeout = window.setTimeout(fetchLibraries, 0); setCallback(timeout); - }, [state]); + }, [state, ready]); return { state, - setLoading, - setLoaded, - setToLoad, isLoaded, isLoading, fetchLibraries, diff --git a/dash/dash-renderer/src/libraries/libraryTypes.ts b/dash/dash-renderer/src/libraries/libraryTypes.ts new file mode 100644 index 0000000000..b1c5f4887c --- /dev/null +++ b/dash/dash-renderer/src/libraries/libraryTypes.ts @@ -0,0 +1,17 @@ +export enum LibrariesActions { + LOAD = 'LOAD_LIBRARY', + LOADED = 'LOADED_LIBRARY', + TO_LOAD = 'TO_LOAD' +} + +export type LibrariesState = { + [libname: string]: { + toLoad: boolean; + loading: boolean; + loaded: boolean; + }; +}; +export type LibraryResource = { + type: '_js_dist' | '_css_dist'; + url: string; +}; diff --git a/dash/dash-renderer/src/libraries/loadLibrary.ts b/dash/dash-renderer/src/libraries/loadLibrary.ts new file mode 100644 index 0000000000..0f83670c1d --- /dev/null +++ b/dash/dash-renderer/src/libraries/loadLibrary.ts @@ -0,0 +1,31 @@ +import {LibraryResource} from './libraryTypes'; + +export default function (resource: LibraryResource) { + let prom; + const head = document.querySelector('head'); + if (resource.type === '_js_dist') { + const element = document.createElement('script'); + element.src = resource.url; + element.async = true; + prom = new Promise((resolve, reject) => { + element.onload = () => { + resolve(); + }; + element.onerror = error => reject(error); + }); + + head?.appendChild(element); + } else if (resource.type === '_css_dist') { + const element = document.createElement('link'); + element.href = resource.url; + element.rel = 'stylesheet'; + prom = new Promise((resolve, reject) => { + element.onload = () => { + resolve(); + }; + element.onerror = error => reject(error); + }); + head?.appendChild(element); + } + return prom; +} diff --git a/dash/dash-renderer/src/reducers/libraries.ts b/dash/dash-renderer/src/reducers/libraries.ts new file mode 100644 index 0000000000..421108b486 --- /dev/null +++ b/dash/dash-renderer/src/reducers/libraries.ts @@ -0,0 +1,60 @@ +import {assocPath, pipe} from 'ramda'; +import {LibrariesActions, LibrariesState} from '../libraries/libraryTypes'; + +type LoadingPayload = { + libraries: string[]; +}; + +type LoadedPayload = { + libraries: string[]; +}; + +type ToLoadPayload = { + library: string; +}; + +type LibrariesAction = { + type: LibrariesActions; + payload: LoadingPayload | LoadedPayload | ToLoadPayload; +}; + +function handleLoad(library: string, state: LibrariesState) { + return pipe( + assocPath([library, 'loading'], true), + assocPath([library, 'toLoad'], false) + )(state) as LibrariesState; +} + +function handleLoaded(library: string, state: LibrariesState) { + return pipe( + assocPath([library, 'loaded'], true), + assocPath([library, 'loading'], false) + )(state) as LibrariesState; +} + +export default function librariesReducer( + state: LibrariesState = {}, + action: LibrariesAction +): LibrariesState { + switch (action.type) { + case LibrariesActions.LOAD: + return (action.payload as LoadingPayload).libraries.reduce( + (acc, lib) => handleLoad(lib, acc), + state + ); + case LibrariesActions.LOADED: + return (action.payload as LoadedPayload).libraries.reduce( + (acc, lib) => handleLoaded(lib, acc), + state + ); + case LibrariesActions.TO_LOAD: + return pipe( + assocPath( + [(action.payload as ToLoadPayload).library, 'toLoad'], + true + ) + )(state) as LibrariesState; + default: + return state; + } +} diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 97b71b6bce..f025d27354 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -18,6 +18,7 @@ import layout from './layout'; import loadingMap from './loadingMap'; import paths from './paths'; import callbackJobs from './callbackJobs'; +import libraries from './libraries'; export const apiRequests = [ 'dependenciesRequest', @@ -47,6 +48,7 @@ function mainReducer() { }, apiRequests); parts.callbackJobs = callbackJobs; + parts.libraries = libraries; return combineReducers(parts); } diff --git a/dash/dash-renderer/src/types/callbacks.ts b/dash/dash-renderer/src/types/callbacks.ts index b98bfd6f00..17cbfbe5cf 100644 --- a/dash/dash-renderer/src/types/callbacks.ts +++ b/dash/dash-renderer/src/types/callbacks.ts @@ -1,3 +1,5 @@ +import {LibraryResource} from '../libraries/libraryTypes'; + type CallbackId = string | {[key: string]: any}; export interface ICallbackDefinition { @@ -102,4 +104,5 @@ export type CallbackResponseData = { running?: CallbackResponse; runningOff?: CallbackResponse; cancel?: ICallbackProperty[]; + resources: LibraryResource[]; }; From b8655c5ee808d257f6227f81cb350b673c7e22c9 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 15 Feb 2024 09:55:15 -0500 Subject: [PATCH 04/17] Fix initial libraries loading. --- dash/dash-renderer/src/libraries/librariesContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts index 9fe6f62f26..6a1ab1a9af 100644 --- a/dash/dash-renderer/src/libraries/librariesContext.ts +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -105,7 +105,7 @@ export function createLibrariesContext( } const libraries = getLibrariesToLoad(); if (!libraries.length) { - if (!ready && initialLibraries.length === 0) { + if (!ready && initialLibraries) { onReady(); } return; @@ -115,7 +115,7 @@ export function createLibrariesContext( } const timeout = window.setTimeout(fetchLibraries, 0); setCallback(timeout); - }, [state, ready]); + }, [state, ready, initialLibraries]); return { state, From 07f44ec2317d5eed79237af2be89d34ced0c1cdb Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 15 Feb 2024 10:51:38 -0500 Subject: [PATCH 05/17] Move ready signal for all loaded. --- dash/dash-renderer/src/libraries/librariesContext.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts index 6a1ab1a9af..825c9efc34 100644 --- a/dash/dash-renderer/src/libraries/librariesContext.ts +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -95,6 +95,9 @@ export function createLibrariesContext( if (loaded.length) { dispatch(setLibraryLoaded({libraries: loaded})); } + if (loaded.length === initialLibraries.length) { + onReady(); + } }); }, [initialLibraries]); @@ -105,9 +108,6 @@ export function createLibrariesContext( } const libraries = getLibrariesToLoad(); if (!libraries.length) { - if (!ready && initialLibraries) { - onReady(); - } return; } if (callback > 0) { From 8c76cd06c32fb8e6c4fc4f7b32dbe0351756650b Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Feb 2024 09:25:43 -0500 Subject: [PATCH 06/17] Fix async/dynamic resources loading. --- dash/dash.py | 16 +++++++--------- tests/integration/test_generation.py | 8 +++++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 4656c53ee8..749d571805 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -809,17 +809,13 @@ def serve_dist(self): libraries = flask.request.get_json() dists = [] for dist_type in ("_js_dist", "_css_dist"): - resources = [ - resource - for resource in ComponentRegistry.get_resources(dist_type, libraries) - if not resource.get("async") and not resource.get("dynamic") - ] - srcs = self._collect_and_register_resources(resources) + resources = ComponentRegistry.get_resources(dist_type, libraries) + srcs = self._collect_and_register_resources(resources, False) for src in srcs: dists.append(dict(type=dist_type, url=src)) return flask.jsonify(dists) - def _collect_and_register_resources(self, resources): + def _collect_and_register_resources(self, resources, include_async=True): # now needs the app context. # template in the necessary component suite JS bundles # add the version number of the package as a query parameter @@ -848,6 +844,8 @@ def _relative_url_path(relative_package_path="", namespace=""): srcs = [] for resource in resources: is_dynamic_resource = resource.get("dynamic", False) + is_async = resource.get("async") is not None + excluded = not include_async and is_async if "relative_package_path" in resource: paths = resource["relative_package_path"] @@ -859,7 +857,7 @@ def _relative_url_path(relative_package_path="", namespace=""): self.registered_paths[resource["namespace"]].add(rel_path) - if not is_dynamic_resource: + if not is_dynamic_resource and not excluded: srcs.append( _relative_url_path( relative_package_path=rel_path, @@ -867,7 +865,7 @@ def _relative_url_path(relative_package_path="", namespace=""): ) ) elif "external_url" in resource: - if not is_dynamic_resource: + if not is_dynamic_resource and not excluded: if isinstance(resource["external_url"], str): srcs.append(resource["external_url"]) else: diff --git a/tests/integration/test_generation.py b/tests/integration/test_generation.py index 1ed4f7ac78..b888c5b2cc 100644 --- a/tests/integration/test_generation.py +++ b/tests/integration/test_generation.py @@ -56,7 +56,13 @@ def test_gene001_simple_callback(dash_duo): def test_gene002_arbitrary_resources(dash_duo): app = Dash(__name__) - app.layout = Div([Button("Click", id="btn"), Div(id="container")]) + app.layout = Div( + [ + Button("Click", id="btn"), + Div(id="container"), + MyStandardComponent(), + ] + ) @app.callback(Output("container", "children"), [Input("btn", "n_clicks")]) def update_container(n_clicks): From 65801b8a94e17fe3eaa19c8c3b6f2702ef22a5f7 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Feb 2024 10:21:23 -0500 Subject: [PATCH 07/17] Cleanup --- dash/dash-renderer/src/TreeContainer.js | 3 --- dash/dash-renderer/src/libraries/librariesContext.ts | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index 9998abcc67..b79980e9a8 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -215,9 +215,6 @@ class BaseTreeContainer extends Component { } validateComponent(_dashprivate_layout); - // console.log("Dash private config", _dashprivate_config); - // const element = Registry.resolve(_dashprivate_layout); - // Hydrate components props const childrenProps = pathOr( [], diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts index 825c9efc34..ed871850c8 100644 --- a/dash/dash-renderer/src/libraries/librariesContext.ts +++ b/dash/dash-renderer/src/libraries/librariesContext.ts @@ -1,4 +1,4 @@ -import {createContext, useContext, useEffect, useState} from 'react'; +import {createContext, useEffect, useState} from 'react'; import {pathOr, toPairs} from 'ramda'; import loadLibrary from './loadLibrary'; import {batch, useDispatch, useSelector} from 'react-redux'; @@ -130,7 +130,3 @@ export function createLibrariesContext( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore export const LibrariesContext = createContext(null); - -export function useDashLibraries() { - return useContext(LibrariesContext); -} From c500a53cc36ac61990f9f0ccf4cdfa779f010f1b Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 19 Feb 2024 10:22:05 -0500 Subject: [PATCH 08/17] Add test dynamic loading. --- tests/integration/renderer/test_libraries.py | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/integration/renderer/test_libraries.py diff --git a/tests/integration/renderer/test_libraries.py b/tests/integration/renderer/test_libraries.py new file mode 100644 index 0000000000..8cfcaa6b7c --- /dev/null +++ b/tests/integration/renderer/test_libraries.py @@ -0,0 +1,53 @@ +from dash import Dash, html, Input, Output +import dash_test_components as dt +import dash_generator_test_component_standard as dgs + + +def test_rblib001_dynamic_loading(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Button("Insert", id="insert-btn"), + html.Div(id="output"), + dgs.MyStandardComponent(id="dgs"), + ] + ) + + @app.callback( + Output("output", "children"), + [Input("insert-btn", "n_clicks")], + prevent_initial_call=True, + ) + def update_output(_): + import dash_generator_test_component_nested as dn + + return [ + dt.StyledComponent(value="Styled", id="styled"), + dn.MyNestedComponent(value="nested", id="nested"), + ] + + dash_duo.start_server(app) + + def assert_unloaded(namespace): + assert dash_duo.driver.execute_script( + f"return window['{namespace}'] === undefined" + ) + + def assert_loaded(namespace): + assert dash_duo.driver.execute_script( + f"return window['{namespace}'] !== undefined" + ) + + assert_unloaded(dt.package_name) + assert_unloaded(dgs.package_name) + assert_unloaded("dash_generator_test_component_nested") + dash_duo.wait_for_element("#dgs") + assert_unloaded(dt.package_name) + assert_loaded(dgs.package_name) + + dash_duo.wait_for_element("#insert-btn").click() + + dash_duo.wait_for_element("#styled") + assert_loaded(dt.package_name) + assert_loaded("dash_generator_test_component_nested") From a223f1bbbd3321e1fe903dc571a290a21cf5bc3e Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 19 Feb 2024 11:12:06 -0500 Subject: [PATCH 09/17] remove initial unloaded on initial layout as dependent on driver performance. --- tests/integration/renderer/test_libraries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/renderer/test_libraries.py b/tests/integration/renderer/test_libraries.py index 8cfcaa6b7c..e7332c99d3 100644 --- a/tests/integration/renderer/test_libraries.py +++ b/tests/integration/renderer/test_libraries.py @@ -40,7 +40,6 @@ def assert_loaded(namespace): ) assert_unloaded(dt.package_name) - assert_unloaded(dgs.package_name) assert_unloaded("dash_generator_test_component_nested") dash_duo.wait_for_element("#dgs") assert_unloaded(dt.package_name) From 42270e8de6103629d65eac0303412b9d1a531e43 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 28 Feb 2024 09:17:44 -0500 Subject: [PATCH 10/17] Fix dynamic loading after callback. --- dash/dash-renderer/src/actions/callbacks.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 134959e69a..f3927ae423 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -517,7 +517,11 @@ function handleServerside( (newData: any) => { Object.values(newData).forEach(newProp => { crawlLayout(newProp, (c: any) => { - if (!libraries.includes(c.namespace)) { + if ( + c.namespace && + !libraries.includes(c.namespace) && + !newLibs.includes(c.namespace) + ) { newLibs.push(c.namespace); } }); From 9b42485319a36b97b2436d0215d0e1bbc4d36cb2 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 28 Feb 2024 09:41:14 -0500 Subject: [PATCH 11/17] any->object. --- @plotly/dash-test-components/src/components/StyledComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@plotly/dash-test-components/src/components/StyledComponent.js b/@plotly/dash-test-components/src/components/StyledComponent.js index 1fc6664d9a..f48dbf7884 100644 --- a/@plotly/dash-test-components/src/components/StyledComponent.js +++ b/@plotly/dash-test-components/src/components/StyledComponent.js @@ -15,7 +15,7 @@ StyledComponent.propTypes = { /** * The style */ - style: PropTypes.any, + style: PropTypes.object, /** * The value to display From 1c94028cb9a70cc457f1b3055a49cd7c9928a593 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 11:29:07 -0500 Subject: [PATCH 12/17] Update changelog. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c993fdd65..bcc8cfbd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2735](https://github.com/plotly/dash/pull/2735) Configure CI for Python 3.8 and 3.12, drop support for Python 3.6 and Python 3.7 [#2736](https://github.com/plotly/dash/issues/2736) ## Added +- []() Add dynamic loading of component libraries. + - Add `dynamic_loading=True` to dash init. + - Add `preloaded_libraries=[]` to dash init, included libraries names will be loaded on the index like before. - [#2758](https://github.com/plotly/dash/pull/2758) - exposing `setProps` to `dash_clientside.clientSide_setProps` to allow for JS code to interact directly with the dash eco-system - [#2730](https://github.com/plotly/dash/pull/2721) Load script files with `.mjs` ending as js modules From 5a3a66400fb605f0b11cdd90ee825016195ade1d Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 11:32:48 -0500 Subject: [PATCH 13/17] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc8cfbd33..1a8431843a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2735](https://github.com/plotly/dash/pull/2735) Configure CI for Python 3.8 and 3.12, drop support for Python 3.6 and Python 3.7 [#2736](https://github.com/plotly/dash/issues/2736) ## Added -- []() Add dynamic loading of component libraries. +- [#2762](https://github.com/plotly/dash/pull/2762) Add dynamic loading of component libraries. - Add `dynamic_loading=True` to dash init. - Add `preloaded_libraries=[]` to dash init, included libraries names will be loaded on the index like before. - [#2758](https://github.com/plotly/dash/pull/2758) From 0ffadba653a01f19eb7c08539b1bb8549825cfd4 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 13:13:44 -0500 Subject: [PATCH 14/17] build From d34bc86d8e2a49e9d80c968a030737592009b77c Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 13:43:52 -0500 Subject: [PATCH 15/17] build From 58bd9b8249ae8b54eb4a7effbe676720a6a28eb0 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 14:36:42 -0500 Subject: [PATCH 16/17] build From 8e4b4fe7c841109633631768b743602b35ff9af9 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 29 Feb 2024 15:08:39 -0500 Subject: [PATCH 17/17] lcbc016 uses lock instead of sleep. --- .../long_callback/app_page_cancel.py | 10 ++-- .../test_basic_long_callback016.py | 46 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/tests/integration/long_callback/app_page_cancel.py b/tests/integration/long_callback/app_page_cancel.py index 5134ad7a14..d99ed6ac82 100644 --- a/tests/integration/long_callback/app_page_cancel.py +++ b/tests/integration/long_callback/app_page_cancel.py @@ -1,7 +1,5 @@ from dash import Dash, Input, Output, dcc, html, page_container, register_page -import time - from tests.integration.long_callback.utils import get_long_callback_manager long_callback_manager = get_long_callback_manager() @@ -23,7 +21,7 @@ page_container, ] ) - +app.test_lock = lock = long_callback_manager.test_lock register_page( "one", @@ -78,7 +76,8 @@ def on_click_no_cancel(_): interval=300, ) def on_click1(n_clicks): - time.sleep(2) + with lock: + pass return f"Click {n_clicks}" @@ -97,7 +96,8 @@ def on_click1(n_clicks): interval=300, ) def on_click1(n_clicks): - time.sleep(2) + with lock: + pass return f"Click {n_clicks}" diff --git a/tests/integration/long_callback/test_basic_long_callback016.py b/tests/integration/long_callback/test_basic_long_callback016.py index 9b8f781ed2..e822a408e4 100644 --- a/tests/integration/long_callback/test_basic_long_callback016.py +++ b/tests/integration/long_callback/test_basic_long_callback016.py @@ -1,5 +1,4 @@ import sys -import time import pytest @@ -12,32 +11,37 @@ def test_lcbc016_multi_page_cancel(dash_duo, manager): with setup_long_callback_app(manager, "app_page_cancel") as app: dash_duo.start_server(app) - dash_duo.find_element("#start1").click() - dash_duo.wait_for_text_to_equal("#progress1", "running") - dash_duo.find_element("#shared_cancel").click() - dash_duo.wait_for_text_to_equal("#progress1", "idle") - time.sleep(2.1) + + with app.test_lock: + dash_duo.find_element("#start1").click() + dash_duo.wait_for_text_to_equal("#progress1", "running") + dash_duo.find_element("#shared_cancel").click() + dash_duo.wait_for_text_to_equal("#progress1", "idle") + dash_duo.wait_for_text_to_equal("#output1", "initial") - dash_duo.find_element("#start1").click() - dash_duo.wait_for_text_to_equal("#progress1", "running") - dash_duo.find_element("#cancel1").click() - dash_duo.wait_for_text_to_equal("#progress1", "idle") - time.sleep(2.1) + with app.test_lock: + dash_duo.find_element("#start1").click() + dash_duo.wait_for_text_to_equal("#progress1", "running") + dash_duo.find_element("#cancel1").click() + dash_duo.wait_for_text_to_equal("#progress1", "idle") + dash_duo.wait_for_text_to_equal("#output1", "initial") dash_duo.server_url = dash_duo.server_url + "/2" - dash_duo.find_element("#start2").click() - dash_duo.wait_for_text_to_equal("#progress2", "running") - dash_duo.find_element("#shared_cancel").click() - dash_duo.wait_for_text_to_equal("#progress2", "idle") - time.sleep(2.1) + with app.test_lock: + dash_duo.find_element("#start2").click() + dash_duo.wait_for_text_to_equal("#progress2", "running") + dash_duo.find_element("#shared_cancel").click() + dash_duo.wait_for_text_to_equal("#progress2", "idle") + dash_duo.wait_for_text_to_equal("#output2", "initial") - dash_duo.find_element("#start2").click() - dash_duo.wait_for_text_to_equal("#progress2", "running") - dash_duo.find_element("#cancel2").click() - dash_duo.wait_for_text_to_equal("#progress2", "idle") - time.sleep(2.1) + with app.test_lock: + dash_duo.find_element("#start2").click() + dash_duo.wait_for_text_to_equal("#progress2", "running") + dash_duo.find_element("#cancel2").click() + dash_duo.wait_for_text_to_equal("#progress2", "idle") + dash_duo.wait_for_text_to_equal("#output2", "initial")