Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Enable storing the callback outputs in the persistence storage #3144

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def callback(
manager=None,
cache_args_to_ignore=None,
on_error: Optional[Callable[[Exception], Any]] = None,
enable_persistence: bool = False,
**_kwargs,
):
"""
Expand Down Expand Up @@ -145,6 +146,10 @@ def callback(
Function to call when the callback raises an exception. Receives the
exception object as first argument. The callback_context can be used
to access the original callback inputs, states and output.
:param enable_persistence:
Indicates whether the callback can write in the persistence storage.
If set to `True`, any outputs with persistence enabled will have their values
stored in the browser's persistence storage when updated by the callback.
"""

long_spec = None
Expand Down Expand Up @@ -195,6 +200,7 @@ def callback(
manager=manager,
running=running,
on_error=on_error,
enable_persistence=enable_persistence,
)


Expand Down Expand Up @@ -237,6 +243,7 @@ def insert_callback(
running=None,
dynamic_creator: Optional[bool] = False,
no_output=False,
enable_persistence: bool = False,
):
if prevent_initial_call is None:
prevent_initial_call = config_prevent_initial_callbacks
Expand All @@ -260,6 +267,7 @@ def insert_callback(
},
"dynamic_creator": dynamic_creator,
"no_output": no_output,
"enable_persistence": enable_persistence,
}
if running:
callback_spec["running"] = running
Expand Down Expand Up @@ -315,6 +323,7 @@ def register_callback(
manager = _kwargs.get("manager")
running = _kwargs.get("running")
on_error = _kwargs.get("on_error")
enable_persistence = _kwargs.get("enable_persistence")
if running is not None:
if not isinstance(running[0], (list, tuple)):
running = [running]
Expand All @@ -340,6 +349,7 @@ def register_callback(
dynamic_creator=allow_dynamic_callbacks,
running=running,
no_output=not has_output,
enable_persistence=enable_persistence,
)

# pylint: disable=too-many-locals
Expand Down Expand Up @@ -602,6 +612,7 @@ def register_clientside_callback(
):
output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs)
no_output = isinstance(output, (list,)) and len(output) == 0
enable_persistence = kwargs.get("enable_persistence")
insert_callback(
callback_list,
callback_map,
Expand All @@ -613,6 +624,7 @@ def register_clientside_callback(
None,
prevent_initial_call,
no_output=no_output,
enable_persistence=enable_persistence,
)

# If JS source is explicitly given, create a namespace and function
Expand Down
8 changes: 1 addition & 7 deletions dash/dash-renderer/src/TreeContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} 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';
Expand Down Expand Up @@ -132,8 +131,7 @@ class BaseTreeContainer extends Component {
}

setProps(newProps) {
const {_dashprivate_dispatch, _dashprivate_path, _dashprivate_layout} =
this.props;
const {_dashprivate_dispatch, _dashprivate_path} = this.props;

const oldProps = this.getLayoutProps();
const {id} = oldProps;
Expand Down Expand Up @@ -163,10 +161,6 @@ class BaseTreeContainer extends Component {
);

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(
Expand Down
18 changes: 16 additions & 2 deletions dash/dash-renderer/src/actions/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {once} from 'ramda';
import {once, path} from 'ramda';
import {createAction} from 'redux-actions';
import {addRequestedCallbacks} from './callbacks';
import {getAppState} from '../reducers/constants';
Expand All @@ -7,6 +7,7 @@ import cookie from 'cookie';
import {validateCallbacksToLayout} from './dependencies';
import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
import {getPath} from './paths';
import {recordUiEdit} from '../persistence';

export const onError = createAction(getAction('ON_ERROR'));
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
Expand All @@ -17,7 +18,20 @@ export const setHooks = createAction(getAction('SET_HOOKS'));
export const setLayout = createAction(getAction('SET_LAYOUT'));
export const setPaths = createAction(getAction('SET_PATHS'));
export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
export const updateProps = createAction(getAction('ON_PROP_CHANGE'));

// Change the variable name of the action
export const onPropChange = createAction(getAction('ON_PROP_CHANGE'));

export function updateProps(payload) {
return (dispatch, getState) => {
const {enable_persistence} = payload;
if (payload.source !== 'response' || enable_persistence) {
const component = path(payload.itempath, getState().layout);
recordUiEdit(component, payload.props, dispatch);
}
dispatch(onPropChange(payload));
};
}

export const dispatchError = dispatch => (message, lines) =>
dispatch(
Expand Down
35 changes: 27 additions & 8 deletions dash/dash-renderer/src/observers/executedCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from 'ramda';

import {IStoreState} from '../store';
import {ThunkDispatch} from 'redux-thunk';
import {AnyAction} from 'redux';

import {
aggregateCallbacks,
Expand Down Expand Up @@ -44,7 +46,11 @@ const observer: IStoreObserverDefinition<IStoreState> = {
callbacks: {executed}
} = getState();

function applyProps(id: any, updatedProps: any) {
function applyProps(
id: any,
updatedProps: any,
enable_persistence: boolean
) {
const {layout, paths} = getState();
const itempath = getPath(paths, id);
if (!itempath) {
Expand All @@ -57,18 +63,22 @@ const observer: IStoreObserverDefinition<IStoreState> = {
updatedProps = prunePersistence(
path(itempath, layout),
updatedProps,
dispatch
dispatch,
enable_persistence
);

// In case the update contains whole components, see if any of
// those components have props to update to persist user edits.
const {props} = applyPersistence({props: updatedProps}, dispatch);
const {props} = applyPersistence(
{props: updatedProps},
dispatch,
enable_persistence
);

dispatch(
(dispatch as ThunkDispatch<any, any, AnyAction>)(
updateProps({
itempath,
props,
source: 'response'
source: 'response',
enable_persistence
})
);

Expand Down Expand Up @@ -102,8 +112,17 @@ const observer: IStoreObserverDefinition<IStoreState> = {
paths: oldPaths
} = getState();

const enable_persistence =
cb.callback.enable_persistence === undefined
? false
: cb.callback.enable_persistence;

// Components will trigger callbacks on their own as required (eg. derived)
const appliedProps = applyProps(parsedId, props);
const appliedProps = applyProps(
parsedId,
props,
enable_persistence
);

// Add callbacks for modified inputs
requestedCallbacks = concat(
Expand Down
82 changes: 52 additions & 30 deletions dash/dash-renderer/src/persistence.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ const getProps = layout => {
};

export function recordUiEdit(layout, newProps, dispatch) {
if (newProps === undefined) {
return;
}

const {
canPersist,
id,
Expand All @@ -316,43 +320,52 @@ export function recordUiEdit(layout, newProps, dispatch) {
persisted_props,
persistence_type
} = getProps(layout);
if (!canPersist || !persistence) {
return;

if (canPersist && persistence) {
forEach(persistedProp => {
const [propName, propPart] = persistedProp.split('.');
if (newProps[propName] !== undefined) {
const storage = getStore(persistence_type, dispatch);
const {extract} = getTransform(element, propName, propPart);
const valsKey = getValsKey(id, persistedProp, persistence);

let originalVal = storage.hasItem(valsKey)
? storage.getItem(valsKey)[1]
: extract(props[propName]);
let newVal = extract(newProps[propName]);

storage.setItem(valsKey, [newVal, originalVal], dispatch);
}
}, persisted_props);
}

forEach(persistedProp => {
const [propName, propPart] = persistedProp.split('.');
if (newProps[propName] !== undefined) {
const storage = getStore(persistence_type, dispatch);
const {extract} = getTransform(element, propName, propPart);

const valsKey = getValsKey(id, persistedProp, persistence);
let originalVal = extract(props[propName]);
const newVal = extract(newProps[propName]);

// mainly for nested props with multiple persisted parts, it's
// possible to have the same value as before - should not store
// in this case.
if (originalVal !== newVal) {
if (storage.hasItem(valsKey)) {
originalVal = storage.getItem(valsKey)[1];
}
const vals =
originalVal === undefined
? [newVal]
: [newVal, originalVal];
storage.setItem(valsKey, vals, dispatch);
// Recursively record UI edits for children
const {children} = props;
if (Array.isArray(children)) {
children.forEach((child, i) => {
if (
type(child) === 'Object' &&
child.props &&
newProps['children'] !== undefined
) {
recordUiEdit(child, newProps['children'][i]['props'], dispatch);
}
}
}, persisted_props);
});
} else if (
type(children) === 'Object' &&
children.props &&
newProps['children'] !== undefined
) {
recordUiEdit(children, newProps['children']['props'], dispatch);
}
}

/*
* Used for entire layouts (on load) or partial layouts (from children
* callbacks) to apply previously-stored UI edits to components
*/
export function applyPersistence(layout, dispatch) {
if (type(layout) !== 'Object' || !layout.props) {
export function applyPersistence(layout, dispatch, enable_persistence) {
if (type(layout) !== 'Object' || !layout.props || enable_persistence) {
return layout;
}

Expand Down Expand Up @@ -447,7 +460,12 @@ function persistenceMods(layout, component, path, dispatch) {
* these override UI-driven edits of those exact props
* but not for props nested inside children
*/
export function prunePersistence(layout, newProps, dispatch) {
export function prunePersistence(
layout,
newProps,
dispatch,
enable_persistence
) {
const {
canPersist,
id,
Expand All @@ -462,7 +480,11 @@ export function prunePersistence(layout, newProps, dispatch) {
propName in newProps ? newProps[propName] : prevVal;
const finalPersistence = getFinal('persistence', persistence);

if (!canPersist || !(persistence || finalPersistence)) {
if (
!canPersist ||
!(persistence || finalPersistence) ||
enable_persistence
) {
return newProps;
}

Expand Down
1 change: 1 addition & 0 deletions dash/dash-renderer/src/types/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ICallbackDefinition {
dynamic_creator?: boolean;
running: any;
no_output?: boolean;
enable_persistence?: boolean;
}

export interface ICallbackProperty {
Expand Down