From 8a5ab9a113a65e0fd7c05aabba87ef0ef7662b8c Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 7 Nov 2024 15:39:46 -0500 Subject: [PATCH 01/35] Improve performance of context components re-rendering --- dash/dash-renderer/src/APIController.react.js | 57 +- dash/dash-renderer/src/TreeContainer.js | 547 ------------------ dash/dash-renderer/src/config.ts | 4 +- dash/dash-renderer/src/reducers/layout.js | 10 +- dash/dash-renderer/src/reducers/reducer.js | 15 +- dash/dash-renderer/src/types/component.ts | 32 + .../src/wrapper/CheckedComponent.tsx | 28 + .../dash-renderer/src/wrapper/DashWrapper.tsx | 425 ++++++++++++++ dash/dash-renderer/src/wrapper/selectors.ts | 46 ++ .../TreeContainer.ts => wrapper/wrapping.ts} | 37 +- 10 files changed, 586 insertions(+), 615 deletions(-) delete mode 100644 dash/dash-renderer/src/TreeContainer.js create mode 100644 dash/dash-renderer/src/types/component.ts create mode 100644 dash/dash-renderer/src/wrapper/CheckedComponent.tsx create mode 100644 dash/dash-renderer/src/wrapper/DashWrapper.tsx create mode 100644 dash/dash-renderer/src/wrapper/selectors.ts rename dash/dash-renderer/src/{utils/TreeContainer.ts => wrapper/wrapping.ts} (78%) diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js index b956e6313c..4395884d69 100644 --- a/dash/dash-renderer/src/APIController.react.js +++ b/dash/dash-renderer/src/APIController.react.js @@ -1,8 +1,8 @@ import {batch, connect} from 'react-redux'; import {includes, isEmpty} from 'ramda'; -import React, {useEffect, useRef, useState, createContext} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; + import PropTypes from 'prop-types'; -import TreeContainer from './TreeContainer'; import GlobalErrorContainer from './components/error/GlobalErrorContainer.react'; import { dispatchError, @@ -19,11 +19,9 @@ import {EventEmitter} from './actions/utils'; import {applyPersistence} from './persistence'; import {getAppState} from './reducers/constants'; import {STATUS} from './constants/constants'; -import {getLoadingState, getLoadingHash} from './utils/TreeContainer'; import wait from './utils/wait'; import isSimpleComponent from './isSimpleComponent'; - -export const DashContext = createContext({}); +import DashWrapper from './wrapper/DashWrapper'; /** * Fire off API calls for initialization @@ -37,8 +35,7 @@ const UnconnectedContainer = props => { dependenciesRequest, error, layoutRequest, - layout, - loadingMap + layout } = props; const [errorLoading, setErrorLoading] = useState(false); @@ -49,18 +46,6 @@ const UnconnectedContainer = props => { } const renderedTree = useRef(false); - const propsRef = useRef({}); - propsRef.current = props; - - const provider = useRef({ - fn: () => ({ - _dashprivate_config: propsRef.current.config, - _dashprivate_dispatch: propsRef.current.dispatch, - _dashprivate_graphs: propsRef.current.graphs, - _dashprivate_loadingMap: propsRef.current.loadingMap - }) - }); - useEffect(storeEffect.bind(null, props, events, setErrorLoading)); useEffect(() => { @@ -97,46 +82,26 @@ const UnconnectedContainer = props => { renderedTree.current = true; content = ( - + <> {Array.isArray(layout) ? ( layout.map((c, i) => isSimpleComponent(c) ? ( c ) : ( - ) ) ) : ( - )} - + ); } else { content =
Loading...
; @@ -242,7 +207,6 @@ UnconnectedContainer.propTypes = { hooks: PropTypes.object, layoutRequest: PropTypes.object, layout: PropTypes.any, - loadingMap: PropTypes.any, history: PropTypes.any, error: PropTypes.object, config: PropTypes.object @@ -256,7 +220,6 @@ const Container = connect( hooks: state.hooks, layoutRequest: state.layoutRequest, layout: state.layout, - loadingMap: state.loadingMap, graphs: state.graphs, history: state.history, error: state.error, diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js deleted file mode 100644 index 25b51bc493..0000000000 --- a/dash/dash-renderer/src/TreeContainer.js +++ /dev/null @@ -1,547 +0,0 @@ -import React, {Component, memo, useContext} from 'react'; -import PropTypes from 'prop-types'; -import Registry from './registry'; -import {propTypeErrorHandler} from './exceptions'; -import { - addIndex, - assoc, - assocPath, - concat, - dissoc, - equals, - isEmpty, - isNil, - has, - keys, - map, - mapObjIndexed, - mergeRight, - omit, - pick, - pickBy, - propOr, - path as rpath, - pathOr, - type -} from 'ramda'; -import {notifyObservers, updateProps, onError} 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, - getLoadingState, - validateComponent -} from './utils/TreeContainer'; -import {DashContext} from './APIController.react'; -import {batch} from 'react-redux'; - -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' && - has('type', obj) && - has('namespace', obj) && - has('props', obj) - ); -} - -const DashWrapper = props => { - const context = useContext(DashContext); - return ( - - ); -}; - -const TreeContainer = memo(DashWrapper); - -class BaseTreeContainer extends Component { - constructor(props) { - super(props); - - this.setProps = this.setProps.bind(this); - } - - createContainer(props, component, path, key = undefined) { - return isSimpleComponent(component) ? ( - component - ) : ( - - ); - } - - setProps(newProps) { - const {_dashprivate_dispatch, _dashprivate_path, _dashprivate_layout} = - this.props; - - const oldProps = this.getLayoutProps(); - const {id} = oldProps; - const {_dash_error, ...rest} = newProps; - const changedProps = pickBy( - (val, key) => !equals(val, oldProps[key]), - rest - ); - - if (_dash_error) { - _dashprivate_dispatch( - onError({ - type: 'frontEnd', - error: _dash_error - }) - ); - } - - if (!isEmpty(changedProps)) { - _dashprivate_dispatch((dispatch, getState) => { - const {graphs} = getState(); - // Identify the modified props that are required for callbacks - const watchedKeys = getWatchedKeys( - id, - keys(changedProps), - graphs - ); - - batch(() => { - // setProps here is triggered by the UI - record these changes - // for persistence - recordUiEdit(_dashprivate_layout, newProps, dispatch); - - // Only dispatch changes to Dash if a watched prop changed - if (watchedKeys.length) { - dispatch( - notifyObservers({ - id, - props: pick(watchedKeys, changedProps) - }) - ); - } - - // Always update this component's props - dispatch( - updateProps({ - props: changedProps, - itempath: _dashprivate_path - }) - ); - }); - }); - } - } - - getChildren(components, path) { - if (isNil(components)) { - return null; - } - - return Array.isArray(components) - ? addIndex(map)( - (component, i) => - this.createContainer( - this.props, - component, - concat(path, ['props', 'children', i]) - ), - components - ) - : this.createContainer( - this.props, - components, - concat(path, ['props', 'children']) - ); - } - - wrapChildrenProp(node, childrenProp) { - if (Array.isArray(node)) { - return node.map((n, i) => - isDryComponent(n) - ? this.createContainer( - this.props, - n, - concat(this.props._dashprivate_path, [ - 'props', - ...childrenProp, - i - ]), - i - ) - : n - ); - } - if (!isDryComponent(node)) { - return node; - } - return this.createContainer( - this.props, - node, - concat(this.props._dashprivate_path, ['props', ...childrenProp]) - ); - } - - getComponent( - _dashprivate_layout, - children, - loading_state, - setProps, - _extraProps - ) { - const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} = - this.props; - - if (isEmpty(_dashprivate_layout)) { - return null; - } - - if (isSimpleComponent(_dashprivate_layout)) { - return _dashprivate_layout; - } - validateComponent(_dashprivate_layout); - - const element = Registry.resolve(_dashprivate_layout); - - // Hydrate components props - const childrenProps = pathOr( - [], - [ - 'children_props', - _dashprivate_layout.namespace, - _dashprivate_layout.type - ], - _dashprivate_config - ); - let props = mergeRight( - dissoc('children', _dashprivate_layout.props), - _extraProps - ); - - for (let i = 0; i < childrenProps.length; i++) { - const childrenProp = childrenProps[i]; - - const handleObject = (obj, opath) => { - return mapObjIndexed( - (node, k) => this.wrapChildrenProp(node, [...opath, k]), - obj - ); - }; - - if (childrenProp.includes('.')) { - let path = childrenProp.split('.'); - let node; - let nodeValue; - if (childrenProp.includes('[]')) { - let frontPath = [], - backPath = [], - found = false, - hasObject = false; - path.forEach(p => { - if (!found) { - if (p.includes('[]')) { - found = true; - if (p.includes('{}')) { - hasObject = true; - frontPath.push( - p.replace('{}', '').replace('[]', '') - ); - } else { - frontPath.push(p.replace('[]', '')); - } - } else if (p.includes('{}')) { - hasObject = true; - frontPath.push(p.replace('{}', '')); - } else { - frontPath.push(p); - } - } else { - if (p.includes('{}')) { - hasObject = true; - backPath.push(p.replace('{}', '')); - } else { - backPath.push(p); - } - } - }); - - node = rpath(frontPath, props); - if (node === undefined || !node?.length) { - continue; - } - const firstNode = rpath(backPath, node[0]); - if (!firstNode) { - continue; - } - - nodeValue = node.map((element, i) => { - const elementPath = concat( - frontPath, - concat([i], backPath) - ); - let listValue; - if (hasObject) { - if (backPath.length) { - listValue = handleObject( - rpath(backPath, element), - elementPath - ); - } else { - listValue = handleObject(element, elementPath); - } - } else { - listValue = this.wrapChildrenProp( - rpath(backPath, element), - elementPath - ); - } - return assocPath(backPath, listValue, element); - }); - path = frontPath; - } else { - if (childrenProp.includes('{}')) { - // Only supports one level of nesting. - const front = []; - let dynamic = []; - let hasBack = false; - const backPath = []; - - for (let j = 0; j < path.length; j++) { - const cur = path[j]; - if (cur.includes('{}')) { - dynamic = concat(front, [ - cur.replace('{}', '') - ]); - if (j < path.length - 1) { - hasBack = true; - } - } else { - if (hasBack) { - backPath.push(cur); - } else { - front.push(cur); - } - } - } - - const dynValue = rpath(dynamic, props); - if (dynValue !== undefined) { - nodeValue = mapObjIndexed( - (d, k) => - this.wrapChildrenProp( - hasBack ? rpath(backPath, d) : d, - hasBack - ? concat( - dynamic, - concat([k], backPath) - ) - : concat(dynamic, [k]) - ), - dynValue - ); - path = dynamic; - } - } else { - node = rpath(path, props); - if (node === undefined) { - continue; - } - nodeValue = this.wrapChildrenProp(node, path); - } - } - props = assocPath(path, nodeValue, props); - } else { - if (childrenProp.includes('{}')) { - let opath = childrenProp.replace('{}', ''); - const isArray = childrenProp.includes('[]'); - if (isArray) { - opath = opath.replace('[]', ''); - } - const node = props[opath]; - - if (node !== undefined) { - if (isArray) { - for (let j = 0; j < node.length; j++) { - const aPath = concat([opath], [j]); - props = assocPath( - aPath, - handleObject(node[j], aPath), - props - ); - } - } else { - props = assoc( - opath, - handleObject(node, [opath]), - props - ); - } - } - } else { - const node = props[childrenProp]; - if (node !== undefined) { - props = assoc( - childrenProp, - this.wrapChildrenProp(node, [childrenProp]), - props - ); - } - } - } - } - - if (type(props.id) === 'Object') { - // Turn object ids (for wildcards) into unique strings. - // Because of the `dissoc` above we're not mutating the layout, - // just the id we pass on to the rendered component - props.id = stringifyId(props.id); - } - const extraProps = { - loading_state: loading_state || NOT_LOADING, - setProps - }; - - return ( - - {_dashprivate_config.props_check ? ( - - ) : ( - createElement(element, props, extraProps, children) - )} - - ); - } - - getLayoutProps() { - return propOr({}, 'props', this.props._dashprivate_layout); - } - - render() { - const { - _dashprivate_layout, - _dashprivate_loadingState, - _dashprivate_path - } = this.props; - - const _extraProps = omit( - [ - 'id', - '_dashprivate_error', - '_dashprivate_layout', - '_dashprivate_loadingState', - '_dashprivate_loadingStateHash', - '_dashprivate_path', - '_dashprivate_config', - '_dashprivate_dispatch', - '_dashprivate_graphs', - '_dashprivate_loadingMap' - ], - this.props - ); - - const layoutProps = this.getLayoutProps(); - - const children = this.getChildren( - layoutProps.children, - _dashprivate_path - ); - - return this.getComponent( - _dashprivate_layout, - children, - _dashprivate_loadingState, - this.setProps, - _extraProps - ); - } -} - -TreeContainer.propTypes = { - _dashprivate_error: PropTypes.any, - _dashprivate_layout: PropTypes.object, - _dashprivate_loadingState: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.bool - ]), - _dashprivate_loadingStateHash: PropTypes.string, - _dashprivate_path: PropTypes.string -}; - -BaseTreeContainer.propTypes = { - ...TreeContainer.propTypes, - _dashprivate_config: PropTypes.object, - _dashprivate_dispatch: PropTypes.func, - _dashprivate_graphs: PropTypes.any, - _dashprivate_loadingMap: PropTypes.any, - _dashprivate_path: PropTypes.array -}; - -export default TreeContainer; diff --git a/dash/dash-renderer/src/config.ts b/dash/dash-renderer/src/config.ts index 00db747aec..55a938e06e 100644 --- a/dash/dash-renderer/src/config.ts +++ b/dash/dash-renderer/src/config.ts @@ -1,4 +1,4 @@ -type Config = { +export type DashConfig = { url_base_pathname: string; requests_pathname_prefix: string; ui: boolean; @@ -23,7 +23,7 @@ type Config = { plotlyjs_url?: string; }; -export default function getConfigFromDOM(): Config { +export default function getConfigFromDOM(): DashConfig { const configElement = document.getElementById('_dash-config'); return JSON.parse( configElement?.textContent ? configElement?.textContent : '{}' diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index 6c082f313c..91b02786a8 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -1,10 +1,10 @@ -import {append, assocPath, includes, lensPath, mergeRight, view} from 'ramda'; +import {includes, mergeRight, path} from 'ramda'; import {getAction} from '../actions/constants'; const layout = (state = {}, action) => { if (action.type === getAction('SET_LAYOUT')) { - return action.payload; + return {...action.payload}; } else if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -12,10 +12,8 @@ const layout = (state = {}, action) => { getAction('ON_PROP_CHANGE') ]) ) { - const propPath = append('props', action.payload.itempath); - const existingProps = view(lensPath(propPath), state); - const mergedProps = mergeRight(existingProps, action.payload.props); - return assocPath(propPath, mergedProps, state); + const component = path(action.payload.itempath, state); + component.props = mergeRight(component.props, action.payload.props); } return state; diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 97b71b6bce..d6208e4c09 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -26,6 +26,18 @@ export const apiRequests = [ 'loginRequest' ]; +function callbackNum(state = 0, action) { + // With the refactor of TreeContainer to DashWrapper + // The props are updated partially and no longer + // trigger the selectors on change. + // By changing this store value we circumvent + // the issue. + if (action.type === 'ON_PROP_CHANGE') { + return state + 1; + } + return state; +} + function mainReducer() { const parts = { appLifecycle, @@ -40,7 +52,8 @@ function mainReducer() { isLoading, layout, loadingMap, - paths + paths, + callbackNum }; forEach(r => { parts[r] = createApiReducer(r); diff --git a/dash/dash-renderer/src/types/component.ts b/dash/dash-renderer/src/types/component.ts new file mode 100644 index 0000000000..55c89c7660 --- /dev/null +++ b/dash/dash-renderer/src/types/component.ts @@ -0,0 +1,32 @@ +export type BaseDashProps = { + id?: string; + [key: string]: any; +}; + +export type DashComponent = { + type: string; + namespace: string; + props: BaseDashProps; +}; + +export type UpdatePropsPayload = { + _dash_error?: any; + [key: string]: any; +}; + +export type EnhancedDashProps = + | BaseDashProps + | { + setProps: (props: any) => void; + }; + +// Layout is either a component of a list of components. +export type DashLayout = DashComponent[] | DashComponent; + +export type DashLayoutPath = (string | number)[]; + +export type DashLoadingState = { + is_loading: boolean; + prop_name?: string; + component_name?: string; +}; diff --git a/dash/dash-renderer/src/wrapper/CheckedComponent.tsx b/dash/dash-renderer/src/wrapper/CheckedComponent.tsx new file mode 100644 index 0000000000..725facaa19 --- /dev/null +++ b/dash/dash-renderer/src/wrapper/CheckedComponent.tsx @@ -0,0 +1,28 @@ +import checkPropTypes from '../checkPropTypes'; +import {propTypeErrorHandler} from '../exceptions'; +import {validateComponent} from './wrapping'; + +type CheckedComponentProps = { + children: JSX.Element; + element: any; + component: any; + props?: any; +}; + +export default function CheckedComponent(p: CheckedComponentProps) { + const {element, props, children, component} = p; + + validateComponent(component); + + const errorMessage = checkPropTypes( + element.propTypes, + props, + 'component prop', + element + ); + if (errorMessage) { + propTypeErrorHandler(errorMessage, props, component.type); + } + + return children; +} diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx new file mode 100644 index 0000000000..bdf04708d9 --- /dev/null +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -0,0 +1,425 @@ +import React, {useMemo, useCallback} from 'react'; +import { + path, + concat, + pickBy, + equals, + keys, + isEmpty, + pick, + assocPath, + pathOr, + mergeRight, + dissoc, + assoc, + mapObjIndexed, + type +} from 'ramda'; +import {useSelector, useDispatch, batch, useStore} from 'react-redux'; + +import ComponentErrorBoundary from '../components/error/ComponentErrorBoundary.react'; +import {DashLayoutPath, UpdatePropsPayload} from '../types/component'; +import {DashConfig} from '../config'; +import {notifyObservers, onError, updateProps} from '../actions'; +import {getWatchedKeys, stringifyId} from '../actions/dependencies'; +import {recordUiEdit} from '../persistence'; +import {createElement, isDryComponent} from './wrapping'; +import Registry from '../registry'; +import isSimpleComponent from '../isSimpleComponent'; +import { + selectDashProps, + selectDashPropsEqualityFn, + selectLoadingState, + selectLoadingStateEqualityFn, + selectConfig +} from './selectors'; +import CheckedComponent from './CheckedComponent'; + +type DashWrapperProps = { + _dashprivate_path: DashLayoutPath; + _dashprivate_error?: any; +}; + +function DashWrapper({ + _dashprivate_path: componentPath, + _dashprivate_error +}: DashWrapperProps) { + const dispatch = useDispatch(); + const store = useStore(); + + // Get the config for the component as props + const config: DashConfig = useSelector(selectConfig); + + // Select both the component and it's props. + const [component, componentProps] = useSelector( + selectDashProps(componentPath), + selectDashPropsEqualityFn + ); + + // Loading state for dcc.Loading + const loading_state = useSelector( + selectLoadingState(componentPath), + selectLoadingStateEqualityFn + ); + + const setProps = (newProps: UpdatePropsPayload) => { + const {id} = componentProps; + const {_dash_error, ...restProps} = newProps; + const oldProps = path( + concat(componentPath, ['props']), + (store.getState() as any).layout + ) as any; + const changedProps = pickBy( + (val, key) => !equals(val, oldProps[key]), + restProps + ); + if (_dash_error) { + dispatch( + onError({ + type: 'frontEnd', + error: _dash_error + }) + ); + } + if (!isEmpty(changedProps)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + dispatch((dispatch, getState) => { + const {graphs} = getState(); + // Identify the modified props that are required for callbacks + const watchedKeys = getWatchedKeys( + id, + keys(changedProps), + graphs + ); + + batch(() => { + // setProps here is triggered by the UI - record these changes + // for persistence + recordUiEdit(component, newProps, dispatch); + + // Only dispatch changes to Dash if a watched prop changed + if (watchedKeys.length) { + dispatch( + notifyObservers({ + id, + props: pick(watchedKeys, changedProps) + }) + ); + } + + // Always update this component's props + dispatch( + updateProps({ + props: changedProps, + itempath: componentPath + }) + ); + }); + }); + } + }; + + const createContainer = useCallback( + (container, containerPath, key = undefined) => { + if (isSimpleComponent(component)) { + return component; + } + return ( + + ); + }, + [] + ); + + const wrapChildrenProp = useCallback( + (node: any, childrenProp: DashLayoutPath) => { + if (Array.isArray(node)) { + return node.map((n, i) => { + if (isDryComponent(n)) { + return createContainer( + n, + concat(componentPath, [ + 'props', + ...childrenProp, + i + ]), + i + ); + } + return n; + }); + } + if (!isDryComponent(node)) { + return node; + } + return createContainer( + node, + concat(componentPath, ['props', ...childrenProp]) + ); + }, + [componentPath] + ); + + const extraProps = { + loading_state, + setProps + }; + + const element = useMemo(() => Registry.resolve(component), [component]); + + const hydratedProps = useMemo(() => { + // Hydrate components props + const childrenProps = pathOr( + [], + ['children_props', component.namespace, component.type], + config + ); + let props = mergeRight(dissoc('children', componentProps), extraProps); + + for (let i = 0; i < childrenProps.length; i++) { + const childrenProp: string = childrenProps[i]; + + const handleObject = (obj: any, opath: DashLayoutPath) => { + return mapObjIndexed( + (node, k) => wrapChildrenProp(node, [...opath, k]), + obj + ); + }; + + if (childrenProp.includes('.')) { + let childrenPath: DashLayoutPath = childrenProp.split('.'); + let node: any; + let nodeValue: any; + if (childrenProp.includes('[]')) { + const frontPath: string[] = [], + backPath: string[] = []; + let found = false, + hasObject = false; + // At first the childrenPath is always a list of strings. + (childrenPath as string[]).forEach(p => { + if (!found) { + if (p.includes('[]')) { + found = true; + if (p.includes('{}')) { + hasObject = true; + frontPath.push( + p.replace('{}', '').replace('[]', '') + ); + } else { + frontPath.push(p.replace('[]', '')); + } + } else if (p.includes('{}')) { + hasObject = true; + frontPath.push(p.replace('{}', '')); + } else { + frontPath.push(p); + } + } else { + if (p.includes('{}')) { + hasObject = true; + backPath.push(p.replace('{}', '')); + } else { + backPath.push(p); + } + } + }); + + node = path(frontPath, props); + if (node === undefined || !node?.length) { + continue; + } + const firstNode = path(backPath, node[0]); + if (!firstNode) { + continue; + } + + nodeValue = node.map((el: any, i: number) => { + const elementPath = concat( + frontPath, + concat([i], backPath) + ); + let listValue; + if (hasObject) { + if (backPath.length) { + listValue = handleObject( + path(backPath, el), + elementPath + ); + } else { + listValue = handleObject(el, elementPath); + } + } else { + listValue = wrapChildrenProp( + path(backPath, el), + elementPath + ); + } + return assocPath(backPath, listValue, el); + }); + childrenPath = frontPath; + } else { + if (childrenProp.includes('{}')) { + // Only supports one level of nesting. + const front = []; + let dynamic: DashLayoutPath = []; + let hasBack = false; + const backPath: DashLayoutPath = []; + + for (let j = 0; j < childrenPath.length; j++) { + const cur = childrenPath[j] as string; + if (cur.includes('{}')) { + dynamic = concat(front, [ + cur.replace('{}', '') + ]); + if (j < childrenPath.length - 1) { + hasBack = true; + } + } else { + if (hasBack) { + backPath.push(cur); + } else { + front.push(cur); + } + } + } + + const dynValue = path(dynamic, props); + if (dynValue !== undefined) { + // too dynamic for proper ts. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + nodeValue = mapObjIndexed( + (d, k) => + wrapChildrenProp( + hasBack ? path(backPath, d) : d, + hasBack + ? concat( + dynamic, + concat([k], backPath) + ) + : concat(dynamic, [k]) + ), + dynValue + ); + childrenPath = dynamic; + } + } else { + node = path(childrenPath, props); + if (node === undefined) { + continue; + } + nodeValue = wrapChildrenProp(node, childrenPath); + } + } + props = assocPath(childrenPath, nodeValue, props); + } else { + if (childrenProp.includes('{}')) { + let opath = childrenProp.replace('{}', ''); + const isArray = childrenProp.includes('[]'); + if (isArray) { + opath = opath.replace('[]', ''); + } + const node = props[opath]; + + if (node !== undefined) { + if (isArray) { + for (let j = 0; j < node.length; j++) { + const aPath = concat([opath], [j]); + props = assocPath( + aPath, + handleObject(node[j], aPath), + props + ); + } + } else { + props = assoc( + opath, + handleObject(node, [opath]), + props + ); + } + } + } else { + const node = props[childrenProp]; + if (node !== undefined) { + props = assoc( + childrenProp, + wrapChildrenProp(node, [childrenProp]), + props + ); + } + } + } + } + if (type(props.id) === 'Object') { + // Turn object ids (for wildcards) into unique strings. + // Because of the `dissoc` above we're not mutating the layout, + // just the id we pass on to the rendered component + props.id = stringifyId(props.id); + } + return props; + }, [componentProps]); + + const hydrated = useMemo(() => { + let hydratedChildren: any; + if (componentProps.children) { + hydratedChildren = wrapChildrenProp(componentProps.children, [ + 'children' + ]); + } + if (config.props_check) { + return ( + + {createElement( + element, + hydratedProps, + extraProps, + hydratedChildren + )} + + ); + } + + return createElement( + element, + hydratedProps, + extraProps, + hydratedChildren + ); + }, [ + element, + component, + hydratedProps, + extraProps, + wrapChildrenProp, + componentProps, + config.props_check + ]); + + return ( + + {hydrated} + + ); +} + +export default DashWrapper; diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts new file mode 100644 index 0000000000..abc3382f01 --- /dev/null +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -0,0 +1,46 @@ +import {path} from 'ramda'; + +import { + DashLayoutPath, + DashComponent, + BaseDashProps, + DashLoadingState +} from '../types/component'; +import {getLoadingState} from './wrapping'; + +type SelectDashProps = [DashComponent, BaseDashProps]; + +export const selectDashProps = + (componentPath: DashLayoutPath) => + (state: any): SelectDashProps => { + const c = path(componentPath, state.layout) as DashComponent; + return [c, c.props]; + }; + +export function selectDashPropsEqualityFn( + [c, p]: SelectDashProps, + [oc, op]: SelectDashProps +) { + return p === op && c === oc; +} + +export const selectLoadingState = + (componentPath: DashLayoutPath) => (state: any) => { + const component = path(componentPath, state.layout); + return getLoadingState(component, componentPath, state.loadingMap); + }; + +export function selectLoadingStateEqualityFn( + lhs: DashLoadingState, + rhs: DashLoadingState +) { + return ( + rhs.is_loading === lhs.is_loading && + lhs.component_name == rhs.component_name && + rhs.prop_name == lhs.prop_name + ); +} + +export function selectConfig(state: any) { + return state.config; +} diff --git a/dash/dash-renderer/src/utils/TreeContainer.ts b/dash/dash-renderer/src/wrapper/wrapping.ts similarity index 78% rename from dash/dash-renderer/src/utils/TreeContainer.ts rename to dash/dash-renderer/src/wrapper/wrapping.ts index 8c0da8f37f..f847186549 100644 --- a/dash/dash-renderer/src/utils/TreeContainer.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -1,4 +1,5 @@ -import {path, type, has} from 'ramda'; +import React from 'react'; +import {mergeRight, type, has, path} from 'ramda'; import Registry from '../registry'; import {stringifyId} from '../actions/dependencies'; @@ -8,7 +9,29 @@ function isLoadingComponent(layout: any) { return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; } -const NULL_LOADING_STATE = false; +export function createElement( + element: any, + props: any, + extraProps: any, + children: any +) { + const allProps = mergeRight(props, extraProps); + if (Array.isArray(children)) { + return React.createElement(element, allProps, ...children); + } + return React.createElement(element, allProps, children); +} + +export function isDryComponent(obj: any) { + return ( + type(obj) === 'Object' && + has('type', obj) && + has('namespace', obj) && + has('props', obj) + ); +} + +const NULL_LOADING_STATE = {is_loading: false}; export function getLoadingState( componentLayout: any, @@ -47,16 +70,6 @@ export function getLoadingState( return NULL_LOADING_STATE; } -export const getLoadingHash = (componentPath: any, loadingMap: any) => - ( - ((loadingMap && - (path(componentPath, loadingMap) as any) - ?.__dashprivate__idprops__) ?? - []) as any[] - ) - .map(({id, property}) => `${id}.${property}`) - .join(','); - export function validateComponent(componentDefinition: any) { if (type(componentDefinition) === 'Array') { throw new Error( From 378fec391aa42f1e285f356d3ef8d3c5bd85d3fc Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 7 Nov 2024 16:10:34 -0500 Subject: [PATCH 02/35] Fix list as layout --- dash/dash-renderer/src/reducers/layout.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index 91b02786a8..0f5e21d544 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -4,6 +4,9 @@ import {getAction} from '../actions/constants'; const layout = (state = {}, action) => { if (action.type === getAction('SET_LAYOUT')) { + if (Array.isArray(action.payload)) { + return [...action.payload]; + } return {...action.payload}; } else if ( includes(action.type, [ From 65ffabd8f2770f236556e8a8a5d2b7d4ad4927fd Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 10 Dec 2024 15:22:50 -0500 Subject: [PATCH 03/35] memo wrapper, merge loading state selector, add back _dashprivate_layout --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 23 +++++++++---------- dash/dash-renderer/src/wrapper/selectors.ts | 9 ++++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index bdf04708d9..c0bece4c13 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useCallback} from 'react'; +import React, {useMemo, useCallback, memo} from 'react'; import { path, concat, @@ -29,8 +29,6 @@ import isSimpleComponent from '../isSimpleComponent'; import { selectDashProps, selectDashPropsEqualityFn, - selectLoadingState, - selectLoadingStateEqualityFn, selectConfig } from './selectors'; import CheckedComponent from './CheckedComponent'; @@ -51,17 +49,11 @@ function DashWrapper({ const config: DashConfig = useSelector(selectConfig); // Select both the component and it's props. - const [component, componentProps] = useSelector( + const [component, componentProps, loading_state] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); - // Loading state for dcc.Loading - const loading_state = useSelector( - selectLoadingState(componentPath), - selectLoadingStateEqualityFn - ); - const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; @@ -172,7 +164,8 @@ function DashWrapper({ const extraProps = { loading_state, - setProps + setProps, + _dashprivate_layout: component }; const element = useMemo(() => Registry.resolve(component), [component]); @@ -422,4 +415,10 @@ function DashWrapper({ ); } -export default DashWrapper; +export default memo( + DashWrapper, + (prevProps, nextProps) => + JSON.stringify(prevProps._dashprivate_path) === + JSON.stringify(nextProps._dashprivate_path) && + prevProps._dashprivate_error === nextProps._dashprivate_error +); diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index abc3382f01..9801e94575 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -8,13 +8,18 @@ import { } from '../types/component'; import {getLoadingState} from './wrapping'; -type SelectDashProps = [DashComponent, BaseDashProps]; +type SelectDashProps = [DashComponent, BaseDashProps, DashLoadingState]; export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { const c = path(componentPath, state.layout) as DashComponent; - return [c, c.props]; + const loading_state = getLoadingState( + c, + componentPath, + state.loadingMap + ); + return [c, c.props, loading_state]; }; export function selectDashPropsEqualityFn( From 011ded9b34a18dff2ceca35c9f72e267495524d5 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 12 Dec 2024 15:30:56 -0500 Subject: [PATCH 04/35] Add get_props, extras to wrapper, set_props takes path --- .../components/ConfirmDialogProvider.react.js | 16 +----- .../src/components/Tabs.react.js | 34 +++++------ dash/dash-renderer/src/APIController.react.js | 28 +++++++-- dash/dash-renderer/src/actions/api.js | 4 +- .../src/utils/clientsideFunctions.ts | 57 +++++++++++++++++-- .../dash-renderer/src/wrapper/DashWrapper.tsx | 20 +++++-- 6 files changed, 108 insertions(+), 51 deletions(-) diff --git a/components/dash-core-components/src/components/ConfirmDialogProvider.react.js b/components/dash-core-components/src/components/ConfirmDialogProvider.react.js index c4e82f6a0d..33c17ce2d2 100644 --- a/components/dash-core-components/src/components/ConfirmDialogProvider.react.js +++ b/components/dash-core-components/src/components/ConfirmDialogProvider.react.js @@ -1,4 +1,3 @@ -import {clone} from 'ramda'; import React from 'react'; import PropTypes from 'prop-types'; @@ -20,26 +19,15 @@ export default class ConfirmDialogProvider extends React.Component { render() { const {displayed, id, setProps, children, loading_state} = this.props; - // Will lose the previous onClick of the child - const wrapClick = child => { - const props = clone(child.props); - props._dashprivate_layout.props.onClick = () => { - setProps({displayed: true}); - }; - - return React.cloneElement(child, props); - }; - return (
setProps({displayed: !displayed})} > - {Array.isArray(children) - ? children.map(wrapClick) - : wrapClick(children)} + {children}
); diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 0857ed1c12..7cb9ca3d80 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -135,8 +135,6 @@ export default class Tabs extends Component { super(props); this.selectHandler = this.selectHandler.bind(this); - this.parseChildrenToArray = this.parseChildrenToArray.bind(this); - this.valueOrDefault = this.valueOrDefault.bind(this); if (!has('value', this.props)) { this.props.setProps({ @@ -150,8 +148,12 @@ export default class Tabs extends Component { return this.props.value; } const children = this.parseChildrenToArray(); - if (children && children[0].props.children) { - return children[0].props.children.props.value || 'tab-1'; + if (children && children.length) { + const firstChildren = window.dash_clientside.get_props( + children[0].props.componentPath, + 'value' + ); + return firstChildren || 'tab-1'; } return 'tab-1'; } @@ -173,6 +175,8 @@ export default class Tabs extends Component { let EnhancedTabs; let selectedTab; + const value = this.valueOrDefault(); + if (this.props.children) { const children = this.parseChildrenToArray(); @@ -183,28 +187,16 @@ export default class Tabs extends Component { // enhance Tab components coming from Dash (as dcc.Tab) with methods needed for handling logic let childProps; - if ( - // disabled is a defaultProp (so it's always set) - // meaning that if it's not set on child.props, the actual - // props we want are lying a bit deeper - which means they - // are coming from Dash - isNil(child.props.disabled) && - child.props._dashprivate_layout && - child.props._dashprivate_layout.props - ) { - // props are coming from Dash - childProps = child.props._dashprivate_layout.props; - } else { - // else props are coming from React (Demo.react.js, or Tabs.test.js) - childProps = child.props; - } + childProps = window.dash_clientside.get_props( + child.props.componentPath + ); if (!childProps.value) { childProps = {...childProps, value: `tab-${index + 1}`}; } // check if this child/Tab is currently selected - if (childProps.value === this.valueOrDefault()) { + if (childProps.value === value) { selectedTab = child; } @@ -213,7 +205,7 @@ export default class Tabs extends Component { key={index} id={childProps.id} label={childProps.label} - selected={this.valueOrDefault() === childProps.value} + selected={value === childProps.value} selectHandler={this.selectHandler} className={childProps.className} style={childProps.style} diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js index 4395884d69..beb0bc7669 100644 --- a/dash/dash-renderer/src/APIController.react.js +++ b/dash/dash-renderer/src/APIController.react.js @@ -71,13 +71,33 @@ const UnconnectedContainer = props => { layoutRequest.status && !includes(layoutRequest.status, [STATUS.OK, 'loading']) ) { - content =
Error loading layout
; + if (config.ui) { + content = ( +
+ ); + } else { + content =
Error loading layout
; + } } else if ( errorLoading || (dependenciesRequest.status && !includes(dependenciesRequest.status, [STATUS.OK, 'loading'])) ) { - content =
Error loading dependencies
; + if (config.ui) { + content = ( +
+ ); + } else { + content = ( +
Error loading dependencies
+ ); + } } else if (appLifecycle === getAppState('HYDRATED')) { renderedTree.current = true; @@ -90,7 +110,7 @@ const UnconnectedContainer = props => { ) : ( ) @@ -98,7 +118,7 @@ const UnconnectedContainer = props => { ) : ( )} diff --git a/dash/dash-renderer/src/actions/api.js b/dash/dash-renderer/src/actions/api.js index aada02d5d4..12fa3e84bb 100644 --- a/dash/dash-renderer/src/actions/api.js +++ b/dash/dash-renderer/src/actions/api.js @@ -116,6 +116,7 @@ export default function apiThunk(endpoint, method, store, id, body) { return json; }); } + const content = await res.text(); logWarningOnce( 'Response is missing header: content-type: application/json' ); @@ -123,7 +124,8 @@ export default function apiThunk(endpoint, method, store, id, body) { type: store, payload: { id, - status: res.status + status: res.status, + content } }); } catch (err) { diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index 88c422fa6f..f7b698b945 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -1,22 +1,37 @@ +import {concat, path} from 'ramda'; import {updateProps, notifyObservers} from '../actions/index'; import {getPath} from '../actions/paths'; -const set_props = (id: string | object, props: {[k: string]: any}) => { +/** + * Set the props of a dash component by id or path. + * + * @param idOrPath Path or id of the dash component to update. + * @param props The props to update. + */ +function set_props( + idOrPath: string | object | string[], + props: {[k: string]: any} +) { const ds = ((window as any).dash_stores = (window as any).dash_stores || []); for (let y = 0; y < ds.length; y++) { const {dispatch, getState} = ds[y]; - const {paths} = getState(); - const componentPath = getPath(paths, id); + let componentPath; + if (!Array.isArray(idOrPath)) { + const {paths} = getState(); + componentPath = getPath(paths, idOrPath); + } else { + componentPath = idOrPath; + } dispatch( updateProps({ props, itempath: componentPath }) ); - dispatch(notifyObservers({id, props})); + dispatch(notifyObservers({id: idOrPath, props})); } -}; +} // Clean url code adapted from https://github.com/braintree/sanitize-url/blob/main/src/constants.ts // to allow for data protocol. @@ -42,7 +57,39 @@ const clean_url = (url: string, fallback = 'about:blank') => { return url; }; +/** + * Get the dash props from a component path or id. + * + * @param componentPathOrId The path or the id of the component to get the props of. + * @param propPath Additional key to get the property instead of plain props. + * @returns + */ +function get_props( + componentPathOrId: string[] | string, + ...propPath: string[] +): any { + const ds = ((window as any).dash_stores = + (window as any).dash_stores || []); + for (let y = 0; y < ds.length; y++) { + const {paths, layout} = ds[y].getState(); + let componentPath; + if (!Array.isArray(componentPathOrId)) { + componentPath = getPath(paths, componentPathOrId); + } else { + componentPath = componentPathOrId; + } + const props = path( + concat(componentPath, ['props', ...propPath]), + layout + ); + if (props !== undefined) { + return props; + } + } +} + const dc = ((window as any).dash_clientside = (window as any).dash_clientside || {}); dc['set_props'] = set_props; dc['clean_url'] = dc['clean_url'] === undefined ? clean_url : dc['clean_url']; +dc['get_props'] = get_props; diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index c0bece4c13..a36d5694ac 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -34,12 +34,20 @@ import { import CheckedComponent from './CheckedComponent'; type DashWrapperProps = { - _dashprivate_path: DashLayoutPath; + /** + * Path of the component in the layout. + */ + componentPath: DashLayoutPath; + /** + * extras props to be merged with the dash props from the store. + */ + extras?: any; _dashprivate_error?: any; }; function DashWrapper({ - _dashprivate_path: componentPath, + componentPath, + extras, _dashprivate_error }: DashWrapperProps) { const dispatch = useDispatch(); @@ -126,7 +134,7 @@ function DashWrapper({ key } _dashprivate_error={_dashprivate_error} - _dashprivate_path={containerPath} + componentPath={containerPath} /> ); }, @@ -165,7 +173,7 @@ function DashWrapper({ const extraProps = { loading_state, setProps, - _dashprivate_layout: component + ...extras }; const element = useMemo(() => Registry.resolve(component), [component]); @@ -418,7 +426,7 @@ function DashWrapper({ export default memo( DashWrapper, (prevProps, nextProps) => - JSON.stringify(prevProps._dashprivate_path) === - JSON.stringify(nextProps._dashprivate_path) && + JSON.stringify(prevProps.componentPath) === + JSON.stringify(nextProps.componentPath) && prevProps._dashprivate_error === nextProps._dashprivate_error ); From 603cbceab63c68d7c31599804c7341a6a681f290 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 13 Dec 2024 13:00:07 -0500 Subject: [PATCH 05/35] get_props -> get_layout --- .../src/components/Tabs.react.js | 21 ++++++++++----- .../src/utils/clientsideFunctions.ts | 27 +++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 7cb9ca3d80..43e1eaf3d2 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -149,10 +149,11 @@ export default class Tabs extends Component { } const children = this.parseChildrenToArray(); if (children && children.length) { - const firstChildren = window.dash_clientside.get_props( - children[0].props.componentPath, - 'value' - ); + const firstChildren = window.dash_clientside.get_layout([ + ...children[0].props.componentPath, + 'props', + 'value', + ]); return firstChildren || 'tab-1'; } return 'tab-1'; @@ -187,9 +188,15 @@ export default class Tabs extends Component { // enhance Tab components coming from Dash (as dcc.Tab) with methods needed for handling logic let childProps; - childProps = window.dash_clientside.get_props( - child.props.componentPath - ); + if (React.isValidElement(child)) { + childProps = window.dash_clientside.get_layout([ + ...child.props.componentPath, + 'props', + ]); + } else { + // In case the selected tab is a string. + childProps = {}; + } if (!childProps.value) { childProps = {...childProps, value: `tab-${index + 1}`}; diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index f7b698b945..be178eafd2 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -1,7 +1,14 @@ -import {concat, path} from 'ramda'; +import {path} from 'ramda'; + import {updateProps, notifyObservers} from '../actions/index'; import {getPath} from '../actions/paths'; +function getStores() { + const stores = ((window as any).dash_stores = + (window as any).dash_stores || []); + return stores; +} + /** * Set the props of a dash component by id or path. * @@ -12,8 +19,7 @@ function set_props( idOrPath: string | object | string[], props: {[k: string]: any} ) { - const ds = ((window as any).dash_stores = - (window as any).dash_stores || []); + const ds = getStores(); for (let y = 0; y < ds.length; y++) { const {dispatch, getState} = ds[y]; let componentPath; @@ -64,12 +70,8 @@ const clean_url = (url: string, fallback = 'about:blank') => { * @param propPath Additional key to get the property instead of plain props. * @returns */ -function get_props( - componentPathOrId: string[] | string, - ...propPath: string[] -): any { - const ds = ((window as any).dash_stores = - (window as any).dash_stores || []); +function get_layout(componentPathOrId: string[] | string): any { + const ds = getStores(); for (let y = 0; y < ds.length; y++) { const {paths, layout} = ds[y].getState(); let componentPath; @@ -78,10 +80,7 @@ function get_props( } else { componentPath = componentPathOrId; } - const props = path( - concat(componentPath, ['props', ...propPath]), - layout - ); + const props = path(componentPath, layout); if (props !== undefined) { return props; } @@ -92,4 +91,4 @@ const dc = ((window as any).dash_clientside = (window as any).dash_clientside || {}); dc['set_props'] = set_props; dc['clean_url'] = dc['clean_url'] === undefined ? clean_url : dc['clean_url']; -dc['get_props'] = get_props; +dc['get_layout'] = get_layout; From e1cbc68833a2d57832fdcd295f12af8b88608333 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 16 Dec 2024 15:02:05 -0500 Subject: [PATCH 06/35] take loading_state out of selector --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 10 +++++++--- dash/dash-renderer/src/wrapper/selectors.ts | 9 ++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index a36d5694ac..76d305b64b 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -23,7 +23,7 @@ import {DashConfig} from '../config'; import {notifyObservers, onError, updateProps} from '../actions'; import {getWatchedKeys, stringifyId} from '../actions/dependencies'; import {recordUiEdit} from '../persistence'; -import {createElement, isDryComponent} from './wrapping'; +import {createElement, getLoadingState, isDryComponent} from './wrapping'; import Registry from '../registry'; import isSimpleComponent from '../isSimpleComponent'; import { @@ -57,7 +57,7 @@ function DashWrapper({ const config: DashConfig = useSelector(selectConfig); // Select both the component and it's props. - const [component, componentProps, loading_state] = useSelector( + const [component, componentProps] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); @@ -171,7 +171,11 @@ function DashWrapper({ ); const extraProps = { - loading_state, + loading_state: getLoadingState( + component, + componentPath, + (store.getState() as any).loadingMap + ), setProps, ...extras }; diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 9801e94575..abc3382f01 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -8,18 +8,13 @@ import { } from '../types/component'; import {getLoadingState} from './wrapping'; -type SelectDashProps = [DashComponent, BaseDashProps, DashLoadingState]; +type SelectDashProps = [DashComponent, BaseDashProps]; export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { const c = path(componentPath, state.layout) as DashComponent; - const loading_state = getLoadingState( - c, - componentPath, - state.loadingMap - ); - return [c, c.props, loading_state]; + return [c, c.props]; }; export function selectDashPropsEqualityFn( From 5424fe0d73cc39d20e79661001be66d820e98aed Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 19 Dec 2024 14:56:30 -0500 Subject: [PATCH 07/35] Rework loading component --- .../src/components/Loading.react.js | 203 ++++++++---------- .../Loading/spinners/CircleSpinner.jsx | 12 +- .../Loading/spinners/CubeSpinner.jsx | 12 +- .../fragments/Loading/spinners/DebugTitle.jsx | 10 + .../Loading/spinners/DefaultSpinner.jsx | 11 +- .../fragments/Loading/spinners/DotSpinner.jsx | 11 +- .../Loading/spinners/GraphSpinner.jsx | 11 +- dash/dash-renderer/src/DashRenderer.js | 2 + dash/dash-renderer/src/actions/callbacks.ts | 10 +- dash/dash-renderer/src/actions/loading.ts | 11 + dash/dash-renderer/src/actions/loadingMap.ts | 7 - dash/dash-renderer/src/dashApi.ts | 6 + .../dash-renderer/src/observers/loadingMap.ts | 73 ------- dash/dash-renderer/src/reducers/loading.ts | 38 ++++ dash/dash-renderer/src/reducers/loadingMap.ts | 18 -- dash/dash-renderer/src/reducers/reducer.js | 6 +- dash/dash-renderer/src/store.ts | 7 +- .../dash-renderer/src/wrapper/DashContext.tsx | 61 ++++++ .../dash-renderer/src/wrapper/DashWrapper.tsx | 20 +- dash/dash-renderer/src/wrapper/selectors.ts | 25 +-- dash/dash-renderer/src/wrapper/wrapping.ts | 49 +---- 21 files changed, 265 insertions(+), 338 deletions(-) create mode 100644 components/dash-core-components/src/fragments/Loading/spinners/DebugTitle.jsx create mode 100644 dash/dash-renderer/src/actions/loading.ts delete mode 100644 dash/dash-renderer/src/actions/loadingMap.ts create mode 100644 dash/dash-renderer/src/dashApi.ts delete mode 100644 dash/dash-renderer/src/observers/loadingMap.ts create mode 100644 dash/dash-renderer/src/reducers/loading.ts delete mode 100644 dash/dash-renderer/src/reducers/loadingMap.ts create mode 100644 dash/dash-renderer/src/wrapper/DashContext.tsx diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 8fdef32970..896729b9a5 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -1,5 +1,7 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useState, useRef, useMemo, useEffect} from 'react'; +import {equals, concat, includes, toPairs} from 'ramda'; import PropTypes from 'prop-types'; + import GraphSpinner from '../fragments/Loading/spinners/GraphSpinner.jsx'; import DefaultSpinner from '../fragments/Loading/spinners/DefaultSpinner.jsx'; import CubeSpinner from '../fragments/Loading/spinners/CubeSpinner.jsx'; @@ -16,12 +18,51 @@ const spinnerComponentOptions = { const getSpinner = spinnerType => spinnerComponentOptions[spinnerType] || DefaultSpinner; -/** - * A Loading component that wraps any other component and displays a spinner until the wrapped component has rendered. - */ -const Loading = ({ +const coveringSpinner = { + visibility: 'visible', + position: 'absolute', + top: '0', + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}; + +const loadingSelector = (componentPath, targetComponents) => state => { + let stringPath = JSON.stringify(componentPath); + // Remove the last ] for easy match + stringPath = stringPath.substring(0, stringPath.length - 1); + const loadingChildren = toPairs(state.loading).reduce( + (acc, [path, load]) => { + if (path.startsWith(stringPath)) { + if (targetComponents) { + const target = targetComponents[load.id]; + if (!target) { + return acc; + } + if (Array.isArray(target)) { + if (!includes(load.property, target)) { + return acc; + } + } else if (load.property !== target) { + return acc; + } + } + return concat(acc, load); + } + return acc; + }, + [] + ); + if (loadingChildren.length) { + return loadingChildren; + } + return null; +}; + +function Loading({ children, - loading_state, display = 'auto', color = '#119DFF', id, @@ -38,91 +79,60 @@ const Loading = ({ delay_show = 0, target_components, custom_spinner, -}) => { - const coveringSpinner = { - visibility: 'visible', - position: 'absolute', - top: '0', - height: '100%', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }; - - /* Overrides default Loading behavior if target_components is set. By default, - * Loading fires when any recursive child enters loading state. This makes loading - * opt-in: Loading animation only enabled when one of target components enters loading state. - */ - const isTarget = () => { - if (!target_components) { - return true; - } - const isMatchingComponent = () => { - return Object.entries(target_components).some( - ([component_name, prop_names]) => { - // Convert prop_names to an array if it's not already - const prop_names_array = Array.isArray(prop_names) - ? prop_names - : [prop_names]; - - return ( - loading_state.component_name === component_name && - (prop_names_array.includes('*') || - prop_names_array.some( - prop_name => - loading_state.prop_name === prop_name - )) - ); - } - ); - }; - return isMatchingComponent; - }; +}) { + const ctx = window.dash_component_api.useDashContext(); + const loading = ctx.useSelector( + loadingSelector(ctx.componentPath, target_components), + equals + ); const [showSpinner, setShowSpinner] = useState(show_initially); const dismissTimer = useRef(); const showTimer = useRef(); - // delay_hide and delay_show is from dash-bootstrap-components dbc.Spinner + const containerStyle = useMemo(() => { + if (showSpinner) { + return {visibility: 'hidden', ...overlay_style, ...parent_style}; + } + return parent_style; + }, [showSpinner, parent_style]); + useEffect(() => { if (display === 'show' || display === 'hide') { setShowSpinner(display === 'show'); return; } - if (loading_state) { - if (loading_state.is_loading) { - // if component is currently loading and there's a dismiss timer active - // we need to clear it. - if (dismissTimer.current) { - dismissTimer.current = clearTimeout(dismissTimer.current); - } - // if component is currently loading but the spinner is not showing and - // there is no timer set to show, then set a timeout to show - if (!showSpinner && !showTimer.current) { - showTimer.current = setTimeout(() => { - setShowSpinner(isTarget()); - showTimer.current = null; - }, delay_show); - } - } else { - // if component is not currently loading and there's a show timer - // active we need to clear it - if (showTimer.current) { - showTimer.current = clearTimeout(showTimer.current); - } - // if component is not currently loading and the spinner is showing and - // there's no timer set to dismiss it, then set a timeout to hide it - if (showSpinner && !dismissTimer.current) { - dismissTimer.current = setTimeout(() => { - setShowSpinner(false); - dismissTimer.current = null; - }, delay_hide); - } + if (loading) { + // if component is currently loading and there's a dismiss timer active + // we need to clear it. + if (dismissTimer.current) { + dismissTimer.current = clearTimeout(dismissTimer.current); + } + // if component is currently loading but the spinner is not showing and + // there is no timer set to show, then set a timeout to show + if (!showSpinner && !showTimer.current) { + showTimer.current = setTimeout(() => { + setShowSpinner(true); + showTimer.current = null; + }, delay_show); + } + } else { + // if component is not currently loading and there's a show timer + // active we need to clear it + if (showTimer.current) { + showTimer.current = clearTimeout(showTimer.current); + } + // if component is not currently loading and the spinner is showing and + // there's no timer set to dismiss it, then set a timeout to hide it + if (showSpinner && !dismissTimer.current) { + dismissTimer.current = setTimeout(() => { + setShowSpinner(false); + dismissTimer.current = null; + }, delay_hide); } } - }, [delay_hide, delay_show, loading_state, display, showSpinner]); + }, [delay_hide, delay_show, loading, display, showSpinner]); const Spinner = showSpinner && getSpinner(spinnerType); @@ -131,18 +141,7 @@ const Loading = ({ style={{position: 'relative', ...parent_style}} className={parent_className} > -
+
{children}
@@ -151,7 +150,7 @@ const Loading = ({
); -}; - -Loading._dashprivate_isLoadingComponent = true; +} Loading.propTypes = { /** @@ -227,24 +224,6 @@ Loading.propTypes = { */ color: PropTypes.string, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Setting display to "show" or "hide" will override the loading state coming from dash-renderer */ diff --git a/components/dash-core-components/src/fragments/Loading/spinners/CircleSpinner.jsx b/components/dash-core-components/src/fragments/Loading/spinners/CircleSpinner.jsx index ab53798ea0..19c61411e5 100644 --- a/components/dash-core-components/src/fragments/Loading/spinners/CircleSpinner.jsx +++ b/components/dash-core-components/src/fragments/Loading/spinners/CircleSpinner.jsx @@ -1,6 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DebugTitle from './DebugTitle.jsx'; + + /** * Spinner created by Tobias Ahlin, https://github.com/tobiasahlin/SpinKit */ @@ -14,12 +17,7 @@ const CircleSpinner = ({ }) => { let debugTitle; if (debug) { - debugTitle = ( -

- Loading {status.component_name} - 's {status.prop_name} -

- ); + debugTitle = status.map((s) => ); } let spinnerClass = fullscreen ? 'dash-spinner-container' : ''; if (className) { @@ -187,7 +185,7 @@ const CircleSpinner = ({ }; CircleSpinner.propTypes = { - status: PropTypes.object, + status: PropTypes.array, color: PropTypes.string, className: PropTypes.string, fullscreen: PropTypes.bool, diff --git a/components/dash-core-components/src/fragments/Loading/spinners/CubeSpinner.jsx b/components/dash-core-components/src/fragments/Loading/spinners/CubeSpinner.jsx index 5736c321f0..bb44c0bbd7 100644 --- a/components/dash-core-components/src/fragments/Loading/spinners/CubeSpinner.jsx +++ b/components/dash-core-components/src/fragments/Loading/spinners/CubeSpinner.jsx @@ -2,15 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import changeColor from 'color'; +import DebugTitle from './DebugTitle.jsx'; + + const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { let debugTitle; if (debug) { - debugTitle = ( -

- Loading {status.component_name} - 's {status.prop_name} -

- ); + debugTitle = status.map((s) => ); } let spinnerClass = fullscreen ? 'dash-spinner-container' : ''; if (className) { @@ -189,7 +187,7 @@ const CubeSpinner = ({status, color, fullscreen, debug, className, style}) => { }; CubeSpinner.propTypes = { - status: PropTypes.object, + status: PropTypes.array, color: PropTypes.string, className: PropTypes.string, fullscreen: PropTypes.bool, diff --git a/components/dash-core-components/src/fragments/Loading/spinners/DebugTitle.jsx b/components/dash-core-components/src/fragments/Loading/spinners/DebugTitle.jsx new file mode 100644 index 0000000000..fa58aaac5a --- /dev/null +++ b/components/dash-core-components/src/fragments/Loading/spinners/DebugTitle.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function DebugTitle({id, property}) { + return ( +

+ Loading #{id} + 's {property} +

+ ) +} diff --git a/components/dash-core-components/src/fragments/Loading/spinners/DefaultSpinner.jsx b/components/dash-core-components/src/fragments/Loading/spinners/DefaultSpinner.jsx index 5770b91429..2ec51bc9a5 100644 --- a/components/dash-core-components/src/fragments/Loading/spinners/DefaultSpinner.jsx +++ b/components/dash-core-components/src/fragments/Loading/spinners/DefaultSpinner.jsx @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DebugTitle from './DebugTitle.jsx'; + /** * Spinner created by Tobias Ahlin, https://github.com/tobiasahlin/SpinKit */ @@ -14,12 +16,7 @@ const DefaultSpinner = ({ }) => { let debugTitle; if (debug) { - debugTitle = ( -

- Loading {status.component_name} - 's {status.prop_name} -

- ); + debugTitle = status.map((s) => ); } let spinnerClass = fullscreen ? 'dash-spinner-container' : ''; if (className) { @@ -112,7 +109,7 @@ const DefaultSpinner = ({ }; DefaultSpinner.propTypes = { - status: PropTypes.object, + status: PropTypes.array, color: PropTypes.string, className: PropTypes.string, fullscreen: PropTypes.bool, diff --git a/components/dash-core-components/src/fragments/Loading/spinners/DotSpinner.jsx b/components/dash-core-components/src/fragments/Loading/spinners/DotSpinner.jsx index 6cf81159dd..1ea1dc1c25 100644 --- a/components/dash-core-components/src/fragments/Loading/spinners/DotSpinner.jsx +++ b/components/dash-core-components/src/fragments/Loading/spinners/DotSpinner.jsx @@ -1,18 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DebugTitle from './DebugTitle.jsx'; + /** * Spinner created by Tobias Ahlin, https://github.com/tobiasahlin/SpinKit */ const DotSpinner = ({status, color, fullscreen, debug, className, style}) => { let debugTitle; if (debug) { - debugTitle = ( -

- Loading {status.component_name} - 's {status.prop_name} -

- ); + debugTitle = status.map((s) => ); } let spinnerClass = fullscreen ? 'dash-spinner-container' : ''; if (className) { @@ -91,7 +88,7 @@ const DotSpinner = ({status, color, fullscreen, debug, className, style}) => { }; DotSpinner.propTypes = { - status: PropTypes.object, + status: PropTypes.array, color: PropTypes.string, className: PropTypes.string, fullscreen: PropTypes.bool, diff --git a/components/dash-core-components/src/fragments/Loading/spinners/GraphSpinner.jsx b/components/dash-core-components/src/fragments/Loading/spinners/GraphSpinner.jsx index 50d32c4629..f4a4e06b32 100644 --- a/components/dash-core-components/src/fragments/Loading/spinners/GraphSpinner.jsx +++ b/components/dash-core-components/src/fragments/Loading/spinners/GraphSpinner.jsx @@ -1,15 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DebugTitle from './DebugTitle.jsx'; + const GraphSpinner = ({status, fullscreen, debug, className, style}) => { let debugTitle; if (debug) { - debugTitle = ( -

- Loading {status.component_name} - 's {status.prop_name} -

- ); + debugTitle = status.map((s) => ); } let spinnerClass = fullscreen ? 'dash-spinner-container' : ''; if (className) { @@ -328,7 +325,7 @@ const GraphSpinner = ({status, fullscreen, debug, className, style}) => { }; GraphSpinner.propTypes = { - status: PropTypes.object, + status: PropTypes.array, color: PropTypes.string, className: PropTypes.string, fullscreen: PropTypes.bool, diff --git a/dash/dash-renderer/src/DashRenderer.js b/dash/dash-renderer/src/DashRenderer.js index d1ce51a5c7..cdda58c518 100644 --- a/dash/dash-renderer/src/DashRenderer.js +++ b/dash/dash-renderer/src/DashRenderer.js @@ -3,6 +3,8 @@ import ReactDOM from 'react-dom'; import AppProvider from './AppProvider.react'; +import './dashApi'; + class DashRenderer { constructor(hooks) { // render Dash Renderer upon initialising! diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 23da0a3ff3..a0a0355fbf 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -50,6 +50,7 @@ import {loadLibrary} from '../utils/libraries'; import {parsePMCId} from './patternMatching'; import {replacePMC} from './patternMatching'; +import {loaded, loading} from './loading'; export const addBlockedCallbacks = createAction( CallbackActionType.AddBlocked @@ -723,6 +724,12 @@ export function executeCallback( } const __execute = async (): Promise => { + const loadingOutputs = outputs.map(out => ({ + path: getPath(paths, out.id), + property: out.property, + id: out.id + })); + dispatch(loading(loadingOutputs)); try { const changedPropIds = keys(cb.changedPropIds); const parsedChangedPropsIds = changedPropIds.map(propId => { @@ -879,11 +886,12 @@ export function executeCallback( break; } } - // we reach here when we run out of retries. return {error: lastError, payload: null}; } catch (error: any) { return {error, payload: null}; + } finally { + dispatch(loaded(loadingOutputs)); } }; diff --git a/dash/dash-renderer/src/actions/loading.ts b/dash/dash-renderer/src/actions/loading.ts new file mode 100644 index 0000000000..3fe7852bf2 --- /dev/null +++ b/dash/dash-renderer/src/actions/loading.ts @@ -0,0 +1,11 @@ +import {createAction} from 'redux-actions'; +import {DashLayoutPath} from '../types/component'; + +export type LoadingPayload = { + path: DashLayoutPath; + property: string; + id: any; +}[]; + +export const loading = createAction('LOADING'); +export const loaded = createAction('LOADED'); diff --git a/dash/dash-renderer/src/actions/loadingMap.ts b/dash/dash-renderer/src/actions/loadingMap.ts deleted file mode 100644 index 60512106ce..0000000000 --- a/dash/dash-renderer/src/actions/loadingMap.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {createAction} from 'redux-actions'; - -import {LoadingMapActionType, LoadingMapState} from '../reducers/loadingMap'; - -export const setLoadingMap = createAction( - LoadingMapActionType.Set -); diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts new file mode 100644 index 0000000000..a5b87f6b83 --- /dev/null +++ b/dash/dash-renderer/src/dashApi.ts @@ -0,0 +1,6 @@ +import {DashContext, useDashContext} from './wrapper/DashContext'; + +(window as any).dash_component_api = { + DashContext, + useDashContext +}; diff --git a/dash/dash-renderer/src/observers/loadingMap.ts b/dash/dash-renderer/src/observers/loadingMap.ts deleted file mode 100644 index 288338ff1c..0000000000 --- a/dash/dash-renderer/src/observers/loadingMap.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {equals, flatten, isEmpty, map, reduce} from 'ramda'; - -import {setLoadingMap} from '../actions/loadingMap'; -import {IStoreObserverDefinition} from '../StoreObserver'; -import {IStoreState} from '../store'; -import {ILayoutCallbackProperty} from '../types/callbacks'; - -const observer: IStoreObserverDefinition = { - observer: ({dispatch, getState}) => { - const { - callbacks: {executing, watched, executed}, - loadingMap, - paths - } = getState(); - - /* - Get the path of all components impacted by callbacks - with states: executing, watched, executed. - - For each path, keep track of all (id,prop) tuples that - are impacted for this node and nested nodes. - */ - - const loadingPaths: ILayoutCallbackProperty[] = flatten( - map( - cb => cb.getOutputs(paths), - [...executing, ...watched, ...executed] - ) - ); - - const nextMap: any = isEmpty(loadingPaths) - ? null - : reduce( - (res, {id, property, path}) => { - let target = res; - const idprop = {id, property}; - - // Assign all affected props for this path and nested paths - target.__dashprivate__idprops__ = - target.__dashprivate__idprops__ || []; - target.__dashprivate__idprops__.push(idprop); - - path.forEach((p, i) => { - target = target[p] = - target[p] ?? - (p === 'children' && - typeof path[i + 1] === 'number' - ? [] - : {}); - - target.__dashprivate__idprops__ = - target.__dashprivate__idprops__ || []; - target.__dashprivate__idprops__.push(idprop); - }); - - // Assign one affected prop for this path - target.__dashprivate__idprop__ = - target.__dashprivate__idprop__ || idprop; - - return res; - }, - {} as any, - loadingPaths - ); - - if (!equals(nextMap, loadingMap)) { - dispatch(setLoadingMap(nextMap)); - } - }, - inputs: ['callbacks.executing', 'callbacks.watched', 'callbacks.executed'] -}; - -export default observer; diff --git a/dash/dash-renderer/src/reducers/loading.ts b/dash/dash-renderer/src/reducers/loading.ts new file mode 100644 index 0000000000..85ecf140bc --- /dev/null +++ b/dash/dash-renderer/src/reducers/loading.ts @@ -0,0 +1,38 @@ +import {assocPath, includes, pathOr, without} from 'ramda'; + +import {LoadingPayload} from '../actions/loading'; + +type LoadingState = { + [k: string]: LoadingPayload[]; +}; + +type LoadingAction = { + type: 'LOADING' | 'LOADED'; + payload: LoadingPayload; +}; + +export default function loading( + state: LoadingState = {}, + action: LoadingAction +) { + switch (action.type) { + case 'LOADED': + return action.payload.reduce((acc, load) => { + const loadPath = [JSON.stringify(load.path)]; + const prev = pathOr([], loadPath, acc); + return assocPath(loadPath, without(prev, [load]), acc); + }, state); + case 'LOADING': + return action.payload.reduce((acc, load) => { + const loadPath = [JSON.stringify(load.path)]; + const prev = pathOr([], loadPath, acc); + if (!includes(load, prev)) { + // duplicate outputs + prev.push(load); + } + return assocPath(loadPath, prev, acc); + }, state); + default: + return state; + } +} diff --git a/dash/dash-renderer/src/reducers/loadingMap.ts b/dash/dash-renderer/src/reducers/loadingMap.ts deleted file mode 100644 index 14981e9268..0000000000 --- a/dash/dash-renderer/src/reducers/loadingMap.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum LoadingMapActionType { - Set = 'LoadingMap.Set' -} - -export interface ILoadingMapAction { - type: LoadingMapActionType.Set; - payload: any; -} - -type LoadingMapState = any; -export {LoadingMapState}; - -const DEFAULT_STATE: LoadingMapState = {}; - -export default ( - state: LoadingMapState = DEFAULT_STATE, - action: ILoadingMapAction -) => (action.type === LoadingMapActionType.Set ? action.payload : state); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index d6208e4c09..f8782cec0f 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -15,9 +15,9 @@ import profile from './profile'; import changed from './changed'; import isLoading from './isLoading'; import layout from './layout'; -import loadingMap from './loadingMap'; import paths from './paths'; import callbackJobs from './callbackJobs'; +import loading from './loading'; export const apiRequests = [ 'dependenciesRequest', @@ -51,9 +51,9 @@ function mainReducer() { changed, isLoading, layout, - loadingMap, paths, - callbackNum + callbackNum, + loading }; forEach(r => { parts[r] = createApiReducer(r); diff --git a/dash/dash-renderer/src/store.ts b/dash/dash-renderer/src/store.ts index 261bdbe891..29b79e2cd4 100644 --- a/dash/dash-renderer/src/store.ts +++ b/dash/dash-renderer/src/store.ts @@ -4,25 +4,24 @@ import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; import {ICallbacksState} from './reducers/callbacks'; -import {LoadingMapState} from './reducers/loadingMap'; import {IsLoadingState} from './reducers/isLoading'; import documentTitle from './observers/documentTitle'; import executedCallbacks from './observers/executedCallbacks'; import executingCallbacks from './observers/executingCallbacks'; import isLoading from './observers/isLoading'; -import loadingMap from './observers/loadingMap'; import prioritizedCallbacks from './observers/prioritizedCallbacks'; import requestedCallbacks from './observers/requestedCallbacks'; import storedCallbacks from './observers/storedCallbacks'; +// FIXME add proper type for the store. export interface IStoreState { callbacks: ICallbacksState; isLoading: IsLoadingState; - loadingMap: LoadingMapState; [key: string]: any; } +// Deprecated export interface IStoreObserver { observer: Observer>; inputs: string[]; @@ -43,9 +42,9 @@ export default class RendererStore { private setObservers = once(() => { const observe = this.storeObserver.observe; + // FIXME Remove observer pattern and refactor to standard reducers/actions/selectors. observe(documentTitle); observe(isLoading); - observe(loadingMap); observe(requestedCallbacks); observe(prioritizedCallbacks); observe(executingCallbacks); diff --git a/dash/dash-renderer/src/wrapper/DashContext.tsx b/dash/dash-renderer/src/wrapper/DashContext.tsx new file mode 100644 index 0000000000..eab27e74a8 --- /dev/null +++ b/dash/dash-renderer/src/wrapper/DashContext.tsx @@ -0,0 +1,61 @@ +import React, {useCallback, useContext, useMemo} from 'react'; +import {useStore, useSelector, useDispatch} from 'react-redux'; +import {pathOr} from 'ramda'; + +import {DashLayoutPath} from '../types/component'; + +type DashContextType = { + componentPath: DashLayoutPath; + + isLoading: () => boolean; + + // Give access to the right store. + useSelector: typeof useSelector; + useDispatch: typeof useDispatch; + useStore: typeof useStore; +}; + +export const DashContext = React.createContext({} as any); + +type DashContextProviderProps = { + children: JSX.Element; + componentPath: DashLayoutPath; +}; + +export function DashContextProvider(props: DashContextProviderProps) { + const {children, componentPath} = props; + + const stringPath = useMemo( + () => JSON.stringify(componentPath), + [componentPath] + ); + const store = useStore(); + + const isLoading = useCallback(() => { + const loading = pathOr( + [], + componentPath, + (store.getState() as any).loading + ); + return loading.length > 0; + }, [stringPath]); + + const ctxValue = useMemo(() => { + return { + componentPath, + isLoading, + + useSelector, + useStore, + useDispatch + }; + }, [stringPath]); + + return ( + {children} + ); +} + +export function useDashContext() { + return useContext(DashContext); +} diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 76d305b64b..abdc7e8339 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -15,7 +15,7 @@ import { mapObjIndexed, type } from 'ramda'; -import {useSelector, useDispatch, batch, useStore} from 'react-redux'; +import {useSelector, useDispatch, batch} from 'react-redux'; import ComponentErrorBoundary from '../components/error/ComponentErrorBoundary.react'; import {DashLayoutPath, UpdatePropsPayload} from '../types/component'; @@ -23,7 +23,7 @@ import {DashConfig} from '../config'; import {notifyObservers, onError, updateProps} from '../actions'; import {getWatchedKeys, stringifyId} from '../actions/dependencies'; import {recordUiEdit} from '../persistence'; -import {createElement, getLoadingState, isDryComponent} from './wrapping'; +import {createElement, isDryComponent} from './wrapping'; import Registry from '../registry'; import isSimpleComponent from '../isSimpleComponent'; import { @@ -32,6 +32,7 @@ import { selectConfig } from './selectors'; import CheckedComponent from './CheckedComponent'; +import {DashContextProvider} from './DashContext'; type DashWrapperProps = { /** @@ -51,7 +52,6 @@ function DashWrapper({ _dashprivate_error }: DashWrapperProps) { const dispatch = useDispatch(); - const store = useStore(); // Get the config for the component as props const config: DashConfig = useSelector(selectConfig); @@ -65,10 +65,7 @@ function DashWrapper({ const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; - const oldProps = path( - concat(componentPath, ['props']), - (store.getState() as any).layout - ) as any; + const oldProps = componentProps; const changedProps = pickBy( (val, key) => !equals(val, oldProps[key]), restProps @@ -171,11 +168,6 @@ function DashWrapper({ ); const extraProps = { - loading_state: getLoadingState( - component, - componentPath, - (store.getState() as any).loadingMap - ), setProps, ...extras }; @@ -422,7 +414,9 @@ function DashWrapper({ error={_dashprivate_error} dispatch={dispatch} > - {hydrated} + + {hydrated} + ); } diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index abc3382f01..7c902d91d6 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -1,12 +1,6 @@ import {path} from 'ramda'; -import { - DashLayoutPath, - DashComponent, - BaseDashProps, - DashLoadingState -} from '../types/component'; -import {getLoadingState} from './wrapping'; +import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component'; type SelectDashProps = [DashComponent, BaseDashProps]; @@ -24,23 +18,6 @@ export function selectDashPropsEqualityFn( return p === op && c === oc; } -export const selectLoadingState = - (componentPath: DashLayoutPath) => (state: any) => { - const component = path(componentPath, state.layout); - return getLoadingState(component, componentPath, state.loadingMap); - }; - -export function selectLoadingStateEqualityFn( - lhs: DashLoadingState, - rhs: DashLoadingState -) { - return ( - rhs.is_loading === lhs.is_loading && - lhs.component_name == rhs.component_name && - rhs.prop_name == lhs.prop_name - ); -} - export function selectConfig(state: any) { return state.config; } diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index f847186549..3514331163 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -1,13 +1,5 @@ import React from 'react'; -import {mergeRight, type, has, path} from 'ramda'; - -import Registry from '../registry'; -import {stringifyId} from '../actions/dependencies'; - -function isLoadingComponent(layout: any) { - validateComponent(layout); - return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; -} +import {mergeRight, type, has} from 'ramda'; export function createElement( element: any, @@ -31,45 +23,6 @@ export function isDryComponent(obj: any) { ); } -const NULL_LOADING_STATE = {is_loading: false}; - -export function getLoadingState( - componentLayout: any, - componentPath: any, - loadingMap: any -) { - if (!loadingMap) { - return NULL_LOADING_STATE; - } - - const loadingFragment: any = path(componentPath, loadingMap); - // Component and children are not loading if there's no loading fragment - // for the component's path in the layout. - if (!loadingFragment) { - return NULL_LOADING_STATE; - } - - const idprop: any = loadingFragment.__dashprivate__idprop__; - if (idprop) { - return { - is_loading: true, - prop_name: idprop.property, - component_name: stringifyId(idprop.id) - }; - } - - const idprops: any = loadingFragment.__dashprivate__idprops__?.[0]; - if (idprops && isLoadingComponent(componentLayout)) { - return { - is_loading: true, - prop_name: idprops.property, - component_name: stringifyId(idprops.id) - }; - } - - return NULL_LOADING_STATE; -} - export function validateComponent(componentDefinition: any) { if (type(componentDefinition) === 'Array') { throw new Error( From 1a56d4f0a0a4901f7305c546eeb0aeb659a3b588 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 19 Dec 2024 16:43:28 -0500 Subject: [PATCH 08/35] Add useLoading --- .../src/components/Clipboard.react.js | 27 +++---------------- .../dash-renderer/src/wrapper/DashContext.tsx | 8 +++++- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/components/dash-core-components/src/components/Clipboard.react.js b/components/dash-core-components/src/components/Clipboard.react.js index c7bf1a2592..23d6a7ada5 100644 --- a/components/dash-core-components/src/components/Clipboard.react.js +++ b/components/dash-core-components/src/components/Clipboard.react.js @@ -14,6 +14,8 @@ function wait(ms) { */ export default class Clipboard extends React.Component { + context = window.dash_component_api.DashContext; + constructor(props) { super(props); this.copyToClipboard = this.copyToClipboard.bind(this); @@ -96,7 +98,7 @@ export default class Clipboard extends React.Component { } async loading() { - while (this.props.loading_state?.is_loading) { + while (this.context.isLoading()) { await wait(100); } } @@ -124,7 +126,7 @@ export default class Clipboard extends React.Component { } render() { - const {id, title, className, style, loading_state} = this.props; + const {id, title, className, style} = this.props; const copyIcon = ; const copiedIcon = ; const btnIcon = this.state.copied ? copiedIcon : copyIcon; @@ -136,9 +138,6 @@ export default class Clipboard extends React.Component { style={style} className={className} onClick={this.onClickHandler} - data-dash-is-loading={ - (loading_state && loading_state.is_loading) || undefined - } > {btnIcon}
@@ -196,24 +195,6 @@ Clipboard.propTypes = { */ className: PropTypes.string, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Dash-assigned callback that gets fired when the value changes. */ diff --git a/dash/dash-renderer/src/wrapper/DashContext.tsx b/dash/dash-renderer/src/wrapper/DashContext.tsx index eab27e74a8..1a14dc57d5 100644 --- a/dash/dash-renderer/src/wrapper/DashContext.tsx +++ b/dash/dash-renderer/src/wrapper/DashContext.tsx @@ -8,6 +8,7 @@ type DashContextType = { componentPath: DashLayoutPath; isLoading: () => boolean; + useLoading: () => boolean; // Give access to the right store. useSelector: typeof useSelector; @@ -34,16 +35,21 @@ export function DashContextProvider(props: DashContextProviderProps) { const isLoading = useCallback(() => { const loading = pathOr( [], - componentPath, + [stringPath], (store.getState() as any).loading ); return loading.length > 0; }, [stringPath]); + const useLoading = useCallback(() => { + return useSelector(state => !!pathOr(false, [stringPath], state)); + }, [stringPath]); + const ctxValue = useMemo(() => { return { componentPath, isLoading, + useLoading, useSelector, useStore, From 1f566122f52abed41c67d033c6cb8d87b9347ec2 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 19 Dec 2024 16:53:10 -0500 Subject: [PATCH 09/35] useLoading in html components --- .../scripts/generate-components.js | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/components/dash-html-components/scripts/generate-components.js b/components/dash-html-components/scripts/generate-components.js index 72ce51a13e..92f69cc761 100644 --- a/components/dash-html-components/scripts/generate-components.js +++ b/components/dash-html-components/scripts/generate-components.js @@ -176,24 +176,6 @@ function generatePropTypes(element, attributes) { '${attributeName}': PropTypes.${propType},`; }, '') + ` - /** - * Object that holds the loading state object coming from dash-renderer - */ - 'loading_state': PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Dash-assigned callback that gets fired when the element is clicked. */ @@ -300,7 +282,9 @@ ${customImport} */ const ${Component} = ({n_clicks = 0, n_clicks_timestamp = -1, ...props}) => { const extraProps = {}; - if(props.loading_state && props.loading_state.is_loading) { + const ctx = window.dash_component_api.useDashContext(); + const loading = ctx.useLoading(); + if (loading) { extraProps['data-dash-is-loading'] = true; } ${customCode} From 9741a69775463e760bef3a77242ae63167e50868 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 19 Dec 2024 17:24:10 -0500 Subject: [PATCH 10/35] Add LoadingDiv --- .../src/fragments/Graph.react.js | 15 ++++----------- .../dash-core-components/src/utils/LoadingDiv.js | 16 ++++++++++++++++ dash/dash-renderer/src/wrapper/DashContext.tsx | 5 ++++- 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 components/dash-core-components/src/utils/LoadingDiv.js diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index bed0975b89..c365fb26bb 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -14,6 +14,7 @@ import { } from 'ramda'; import PropTypes from 'prop-types'; import {graphPropTypes, graphDefaultProps} from '../components/Graph.react'; +import LoadingDiv from '../utils/LoadingDiv'; /* global Plotly:true */ /** @@ -514,18 +515,10 @@ class PlotlyGraph extends Component { } render() { - const {className, id, style, loading_state} = this.props; + const {className, id, style} = this.props; return ( -
+
-
+
); } } diff --git a/components/dash-core-components/src/utils/LoadingDiv.js b/components/dash-core-components/src/utils/LoadingDiv.js new file mode 100644 index 0000000000..e63e7be4a2 --- /dev/null +++ b/components/dash-core-components/src/utils/LoadingDiv.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function LoadingDiv({children, ...props}) { + const ctx = window.dash_component_api.useDashContext(); + const loading = ctx.useLoading(); + return ( +
+ {children} +
+ ); +} + +LoadingDiv.propTypes = { + children: PropTypes.node, +}; diff --git a/dash/dash-renderer/src/wrapper/DashContext.tsx b/dash/dash-renderer/src/wrapper/DashContext.tsx index 1a14dc57d5..6a4d00cd12 100644 --- a/dash/dash-renderer/src/wrapper/DashContext.tsx +++ b/dash/dash-renderer/src/wrapper/DashContext.tsx @@ -42,7 +42,10 @@ export function DashContextProvider(props: DashContextProviderProps) { }, [stringPath]); const useLoading = useCallback(() => { - return useSelector(state => !!pathOr(false, [stringPath], state)); + return useSelector((state: any) => { + const load = pathOr([], [stringPath], state.loading); + return load.length > 0; + }); }, [stringPath]); const ctxValue = useMemo(() => { From 957ec39847b359ae5dfcdc73ec8317e47446edb0 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 20 Dec 2024 12:18:23 -0500 Subject: [PATCH 11/35] replace loading for data-dash-is-loading --- .../src/components/Checklist.react.js | 32 +++---------------- .../src/components/Clipboard.react.js | 6 ++-- .../components/ConfirmDialogProvider.react.js | 28 +++------------- .../src/components/DatePickerRange.react.js | 18 ----------- .../src/components/DatePickerSingle.react.js | 18 ----------- .../src/components/Dropdown.react.js | 18 ----------- .../src/components/Graph.react.js | 18 ----------- .../src/components/Input.react.js | 26 ++------------- .../src/components/Link.react.js | 20 ++++-------- .../src/components/Markdown.react.js | 18 ----------- .../src/components/RadioItems.react.js | 31 ++---------------- .../src/components/Slider.react.js | 18 ----------- .../src/components/Tab.react.js | 18 ----------- .../src/components/Tabs.react.js | 28 ++-------------- .../src/components/Tooltip.react.js | 31 +++++------------- .../src/components/Upload.react.js | 18 ----------- .../src/fragments/DatePickerRange.react.js | 9 ++---- .../src/fragments/DatePickerSingle.react.js | 9 ++---- .../src/fragments/Dropdown.react.js | 8 ++--- .../src/fragments/Graph.react.js | 13 ++++++-- .../src/fragments/Markdown.react.js | 9 ++---- .../src/fragments/RangeSlider.react.js | 9 ++---- .../src/fragments/Slider.react.js | 9 ++---- .../src/utils/LoadingDiv.js | 16 ---------- .../src/utils/LoadingElement.js | 28 ++++++++++++++++ 25 files changed, 92 insertions(+), 364 deletions(-) delete mode 100644 components/dash-core-components/src/utils/LoadingDiv.js create mode 100644 components/dash-core-components/src/utils/LoadingElement.js diff --git a/components/dash-core-components/src/components/Checklist.react.js b/components/dash-core-components/src/components/Checklist.react.js index 784d8fc596..dbe8580b27 100644 --- a/components/dash-core-components/src/components/Checklist.react.js +++ b/components/dash-core-components/src/components/Checklist.react.js @@ -1,7 +1,9 @@ import PropTypes from 'prop-types'; import {append, includes, without} from 'ramda'; import React, {Component} from 'react'; + import {sanitizeOptions} from '../utils/optionTypes'; +import LoadingElement from '../utils/LoadingElement'; /** * Checklist is a component that encapsulates several checkboxes. @@ -21,19 +23,11 @@ export default class Checklist extends Component { options, setProps, style, - loading_state, value, inline, } = this.props; return ( -
+ {sanitizeOptions(options).map(option => { return (
+ ); } } @@ -192,24 +186,6 @@ Checklist.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Clipboard.react.js b/components/dash-core-components/src/components/Clipboard.react.js index 23d6a7ada5..4356f978b4 100644 --- a/components/dash-core-components/src/components/Clipboard.react.js +++ b/components/dash-core-components/src/components/Clipboard.react.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCopy, faCheckCircle} from '@fortawesome/free-regular-svg-icons'; +import LoadingElement from '../utils/LoadingElement'; + const clipboardAPI = navigator.clipboard; function wait(ms) { @@ -132,7 +134,7 @@ export default class Clipboard extends React.Component { const btnIcon = this.state.copied ? copiedIcon : copyIcon; return clipboardAPI ? ( -
{btnIcon} -
+ ) : null; } } diff --git a/components/dash-core-components/src/components/ConfirmDialogProvider.react.js b/components/dash-core-components/src/components/ConfirmDialogProvider.react.js index 33c17ce2d2..fe9b2efe1d 100644 --- a/components/dash-core-components/src/components/ConfirmDialogProvider.react.js +++ b/components/dash-core-components/src/components/ConfirmDialogProvider.react.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ConfirmDialog from './ConfirmDialog.react'; +import LoadingElement from '../utils/LoadingElement'; /** * A wrapper component that will display a confirmation dialog @@ -17,19 +18,16 @@ import ConfirmDialog from './ConfirmDialog.react'; */ export default class ConfirmDialogProvider extends React.Component { render() { - const {displayed, id, setProps, children, loading_state} = this.props; + const {displayed, id, setProps, children} = this.props; return ( -
setProps({displayed: !displayed})} > {children} -
+ ); } } @@ -82,22 +80,4 @@ ConfirmDialogProvider.propTypes = { * The children to hijack clicks from and display the popup. */ children: PropTypes.any, - - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), }; diff --git a/components/dash-core-components/src/components/DatePickerRange.react.js b/components/dash-core-components/src/components/DatePickerRange.react.js index e9bcdebd5b..3cdfe83551 100644 --- a/components/dash-core-components/src/components/DatePickerRange.react.js +++ b/components/dash-core-components/src/components/DatePickerRange.react.js @@ -222,24 +222,6 @@ DatePickerRange.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/DatePickerSingle.react.js b/components/dash-core-components/src/components/DatePickerSingle.react.js index 792e9f6b75..1c35fa8505 100644 --- a/components/dash-core-components/src/components/DatePickerSingle.react.js +++ b/components/dash-core-components/src/components/DatePickerSingle.react.js @@ -181,24 +181,6 @@ DatePickerSingle.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Dropdown.react.js b/components/dash-core-components/src/components/Dropdown.react.js index 37111fc338..d0a10fd2a7 100644 --- a/components/dash-core-components/src/components/Dropdown.react.js +++ b/components/dash-core-components/src/components/Dropdown.react.js @@ -177,24 +177,6 @@ Dropdown.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Graph.react.js b/components/dash-core-components/src/components/Graph.react.js index ff326b6b0c..770c9defd6 100644 --- a/components/dash-core-components/src/components/Graph.react.js +++ b/components/dash-core-components/src/components/Graph.react.js @@ -556,24 +556,6 @@ PlotlyGraph.propTypes = { * Function that updates the state tree. */ setProps: PropTypes.func, - - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), }; ControlledPlotlyGraph.propTypes = PlotlyGraph.propTypes; diff --git a/components/dash-core-components/src/components/Input.react.js b/components/dash-core-components/src/components/Input.react.js index d4656fddae..16fa7e611d 100644 --- a/components/dash-core-components/src/components/Input.react.js +++ b/components/dash-core-components/src/components/Input.react.js @@ -3,6 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import isNumeric from 'fast-isnumeric'; import './css/input.css'; +import LoadingElement from '../utils/LoadingElement'; // eslint-disable-next-line no-implicit-coercion const convert = val => (isNumeric(val) ? +val : NaN); @@ -94,14 +95,11 @@ export default class Input extends PureComponent { render() { const valprops = this.props.type === 'number' ? {} : {value: this.state.value}; - const {loading_state} = this.props; let {className} = this.props; className = 'dash-input' + (className ? ` ${className}` : ''); return ( - { - const { - className, - style, - id, - href, - loading_state, - children, - title, - target, - setProps, - } = props; + const {className, style, id, href, children, title, target, setProps} = + props; const cleanUrl = window.dash_clientside.clean_url; const sanitizedUrl = useMemo(() => { return href ? cleanUrl(href) : undefined; }, [href]); + const ctx = window.dash_component_api.useDashContext(); + const loading = ctx.useLoading(); + const updateLocation = e => { const hasModifiers = e.metaKey || e.shiftKey || e.altKey || e.ctrlKey; @@ -80,9 +74,7 @@ const Link = ({refresh = false, ...props}) => { return ( + {sanitizeOptions(options).map(option => (
+ ); } } @@ -185,24 +178,6 @@ RadioItems.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Slider.react.js b/components/dash-core-components/src/components/Slider.react.js index 7e642591c2..2bdfb8e958 100644 --- a/components/dash-core-components/src/components/Slider.react.js +++ b/components/dash-core-components/src/components/Slider.react.js @@ -173,24 +173,6 @@ Slider.propTypes = { */ setProps: PropTypes.func, - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Tab.react.js b/components/dash-core-components/src/components/Tab.react.js index 1985b884c7..273ea16286 100644 --- a/components/dash-core-components/src/components/Tab.react.js +++ b/components/dash-core-components/src/components/Tab.react.js @@ -74,24 +74,6 @@ Tab.propTypes = { * Overrides the default (inline) styles for the Tab component when it is selected. */ selected_style: PropTypes.object, - - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), }; export default Tab; diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 43e1eaf3d2..994d1bd410 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -6,6 +6,7 @@ import {has, is, isNil} from 'ramda'; // some weird interaction btwn styled-jsx 3.4 and babel // see https://github.com/vercel/styled-jsx/pull/716 import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars +import LoadingElement from '../utils/LoadingElement'; // EnhancedTab is defined here instead of in Tab.react.js because if exported there, // it will mess up the Python imports and metadata.json @@ -247,12 +248,7 @@ export default class Tabs extends Component { : 'tab-parent'; return ( -
-
+ ); } } @@ -411,24 +407,6 @@ Tabs.propTypes = { background: PropTypes.string, }), - /** - * Object that holds the loading state object coming from dash-renderer - */ - loading_state: PropTypes.shape({ - /** - * Determines if the component is loading or not - */ - is_loading: PropTypes.bool, - /** - * Holds which property is loading - */ - prop_name: PropTypes.string, - /** - * Holds the name of the component that is loading - */ - component_name: PropTypes.string, - }), - /** * Used to allow user interactions in this component to be persisted when * the component - or the page - is refreshed. If `persisted` is truthy and diff --git a/components/dash-core-components/src/components/Tooltip.react.js b/components/dash-core-components/src/components/Tooltip.react.js index 1d2fcb7b0c..3c5505df43 100644 --- a/components/dash-core-components/src/components/Tooltip.react.js +++ b/components/dash-core-components/src/components/Tooltip.react.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars +import LoadingElement from '../utils/LoadingElement'; /** * A tooltip with an absolute position. @@ -17,15 +18,17 @@ const Tooltip = ({ loading_text = 'Loading...', ...props }) => { - const {bbox, id, loading_state} = props; - const is_loading = loading_state?.is_loading; + const {bbox, id} = props; const show_tooltip = show && bbox; + const ctx = window.dash_component_api.useDashContext(); + const is_loading = ctx.useLoading(); + return ( <>
- - +