dash-renderer
Version:
render dash components in react
424 lines (415 loc) • 17.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.applyPersistence = applyPersistence;
exports.prunePersistence = prunePersistence;
exports.recordUiEdit = recordUiEdit;
exports.stores = exports.storePrefix = void 0;
var _ramda = require("ramda");
var _reduxActions = require("redux-actions");
var _registry = _interopRequireDefault(require("./registry"));
var _dependencies = require("./actions/dependencies");
var _wrapping = require("./wrapper/wrapping");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /**
* Generalized persistence for component props
*
* When users input new prop values, they can be stored and reapplied later,
* when the component is recreated (changing `Tab` for example) or when the
* page is reloaded (depending on `persistence_type`). Storage is tied to
* component ID, and the prop values will not be stored with components
* without an ID.
*
* Renderer handles the mechanics, but components must define a few props:
*
* - `persistence`: boolean, string, or number. For simple usage, set to `true`
* to enable persistence, omit or set `false` to disable. For more complex
* scenarios, use any truthy value, and change to a *different* truthy value
* when you want the persisted values cleared. (modeled off `uirevision` in)
* plotly.js
* Typically should have no default, but the other persistence props should
* have defaults, so all a user needs to do to enable persistence is set this
* one prop.
*
* - `persisted_props`: array of prop names or "nested prop IDs" allowed to
* persist. Normally should default to the full list of supported props,
* so they can all be enabled at once. The main exception to this is if
* there's a prop that *can* be persisted but most users wouldn't want this.
* A nested prop ID describes *part* of a prop to store. It must be
* "<propName>.<piece>" where propName is the prop that has this info, and
* piece may or may not map to the exact substructure being stored but is
* meaningful to the user. For example, in `dash_table`, `columns.name`
* stores `columns[i].name` for all columns `i`. Nested props also need
* entries in `persistenceTransforms` - see below.
*
* - `persistence_type`: one of "local", "session", or "memory", just like
* `dcc.Store`. But the default here should be "local" because the main use
* case is to maintain settings across reloads.
*
* If any `persisted_props` are nested prop IDs, the component should define a
* class property (not a React prop) `persistenceTransforms`, as an object:
* {
* [propName]: {
* [piece]: {
* extract: propValue => valueToStore,
* apply: (storedValue, propValue) => newPropValue
* }
* }
* }
* - `extract` turns a prop value into a reduced value to store.
* - `apply` puts an extracted value back into the prop. Make sure this creates
* a new object rather than mutating `proValue`, and that if there are
* multiple `piece` entries for one `propName`, their `apply` functions
* commute - which should not be an issue if they extract and apply
* non-intersecting parts of the full prop.
* You only need to define these for the props that need them.
* It's important that `extract` pulls out *only* the relevant pieces of the
* prop, because persistence is only maintained if the extracted value of the
* prop before applying persistence is the same as it was before the user's
* changes.
*/
var storePrefix = exports.storePrefix = '_dash_persistence.';
function err(e) {
var error = typeof e === 'string' ? new Error(e) : e;
return (0, _reduxActions.createAction)('ON_ERROR')({
type: 'frontEnd',
error
});
}
/*
* Does a key fit this prefix? Must either be an exact match
* or, if a separator is provided, a scoped match - exact prefix
* followed by the separator (then anything else)
*/
function keyPrefixMatch(prefix, separator) {
var fullStr = prefix + separator;
var fullLen = fullStr.length;
return key => key === prefix || key.substr(0, fullLen) === fullStr;
}
var UNDEFINED = 'U';
var _parse = val => val === UNDEFINED ? undefined : JSON.parse(val || null);
var _stringify = val => val === undefined ? UNDEFINED : JSON.stringify(val);
class WebStore {
constructor(backEnd) {
this._name = backEnd;
this._storage = window[backEnd];
}
hasItem(key) {
return this._storage.getItem(storePrefix + key) !== null;
}
getItem(key) {
// note: _storage.getItem returns null on missing keys
// and JSON.parse(null) returns null as well
return _parse(this._storage.getItem(storePrefix + key));
}
_setItem(key, value) {
// unprotected version of setItem, for use by tryGetWebStore
this._storage.setItem(storePrefix + key, _stringify(value));
}
/*
* In addition to the regular key->value to set, setItem takes
* dispatch as a parameter, so it can report OOM to devtools
*/
setItem(key, value, dispatch) {
try {
this._setItem(key, value);
} catch (e) {
dispatch(err("".concat(key, " failed to save in ").concat(this._name, ". Persisted props may be lost.")));
// TODO: at some point we may want to convert this to fall back
// on memory, pulling out all persistence keys and putting them
// in a MemStore that gets used from then onward.
}
}
removeItem(key) {
this._storage.removeItem(storePrefix + key);
}
/*
* clear matching keys matching (optionally followed by a dot and more
* characters) - or all keys associated with this store if no prefix.
*/
clear(keyPrefix) {
var fullPrefix = storePrefix + (keyPrefix || '');
var keyMatch = keyPrefixMatch(fullPrefix, keyPrefix ? '.' : '');
var keysToRemove = [];
// 2-step process, so we don't depend on any particular behavior of
// key order while removing some
for (var i = 0; i < this._storage.length; i++) {
var fullKey = this._storage.key(i);
if (keyMatch(fullKey)) {
keysToRemove.push(fullKey);
}
}
(0, _ramda.forEach)(k => this._storage.removeItem(k), keysToRemove);
}
}
class MemStore {
constructor() {
this._data = {};
}
hasItem(key) {
return key in this._data;
}
getItem(key) {
// run this storage through JSON too so we know we get a fresh object
// each retrieval
return _parse(this._data[key]);
}
setItem(key, value) {
this._data[key] = _stringify(value);
}
removeItem(key) {
delete this._data[key];
}
clear(keyPrefix) {
if (keyPrefix) {
(0, _ramda.forEach)(key => delete this._data[key], (0, _ramda.filter)(keyPrefixMatch(keyPrefix, '.'), (0, _ramda.keys)(this._data)));
} else {
this._data = {};
}
}
}
// Make a string 2^16 characters long (*2 bytes/char = 130kB), to test storage.
// That should be plenty for common persistence use cases,
// without getting anywhere near typical browser limits
var pow = 16;
function longString() {
var s = 'Spam';
for (var i = 2; i < pow; i++) {
s += s;
}
return s;
}
var stores = exports.stores = {
memory: new MemStore()
// Defer testing & making local/session stores until requested.
// That way if we have errors here they can show up in devtools.
};
var backEnds = {
local: 'localStorage',
session: 'sessionStorage'
};
function tryGetWebStore(backEnd, dispatch) {
var store = new WebStore(backEnd);
var fallbackStore = stores.memory;
var storeTest = longString();
var testKey = storePrefix + 'x.x';
try {
store._setItem(testKey, storeTest);
if (store.getItem(testKey) !== storeTest) {
dispatch(err("".concat(backEnd, " init failed set/get, falling back to memory")));
return fallbackStore;
}
store.removeItem(testKey);
return store;
} catch (e) {
dispatch(err("".concat(backEnd, " init first try failed; clearing and retrying")));
}
try {
store.clear();
store._setItem(testKey, storeTest);
if (store.getItem(testKey) !== storeTest) {
throw new Error('nope');
}
store.removeItem(testKey);
dispatch(err("".concat(backEnd, " init set/get succeeded after clearing!")));
return store;
} catch (e) {
dispatch(err("".concat(backEnd, " init still failed, falling back to memory")));
return fallbackStore;
}
}
function getStore(type, dispatch) {
if (!stores[type]) {
stores[type] = tryGetWebStore(backEnds[type], dispatch);
}
return stores[type];
}
var noopTransform = {
extract: propValue => propValue,
apply: (storedValue, _propValue) => storedValue
};
var getTransform = (element, propName, propPart) => {
if (element.persistenceTransforms && element.persistenceTransforms[propName]) {
if (propPart) {
return element.persistenceTransforms[propName][propPart];
}
return element.persistenceTransforms[propName];
}
return noopTransform;
};
var getValsKey = (id, persistedProp, persistence) => "".concat((0, _dependencies.stringifyId)(id), ".").concat(persistedProp, ".").concat(JSON.stringify(persistence));
var getProps = layout => {
var props = layout.props,
type = layout.type,
namespace = layout.namespace;
if (!type || !namespace) {
// not a real component - just need the props for recursion
return {
props
};
}
var id = props.id,
persistence = props.persistence;
var element = _registry.default.resolve(layout);
var getVal = prop => props[prop] || (element.defaultProps || element.dashPersistence || {})[prop];
var persisted_props = getVal('persisted_props');
var persistence_type = getVal('persistence_type');
var canPersist = id && persisted_props && persistence_type;
return {
canPersist,
id,
props,
element,
persistence,
persisted_props,
persistence_type
};
};
function recordUiEdit(layout, newProps, dispatch) {
var _getProps = getProps(layout),
canPersist = _getProps.canPersist,
id = _getProps.id,
props = _getProps.props,
element = _getProps.element,
persistence = _getProps.persistence,
persisted_props = _getProps.persisted_props,
persistence_type = _getProps.persistence_type;
// if the "persistence" property is changed as a callback output,
// skip the persistence storage overwriting.
var isPersistenceMismatch = (newProps === null || newProps === void 0 ? void 0 : newProps.persistence) !== undefined && newProps.persistence !== persistence;
if (!canPersist || !persistence || isPersistenceMismatch) {
return;
}
(0, _ramda.forEach)(persistedProp => {
var _persistedProp$split = persistedProp.split('.'),
_persistedProp$split2 = _slicedToArray(_persistedProp$split, 2),
propName = _persistedProp$split2[0],
propPart = _persistedProp$split2[1];
if (newProps[propName] !== undefined) {
var storage = getStore(persistence_type, dispatch);
var _getTransform = getTransform(element, propName, propPart),
extract = _getTransform.extract;
var valsKey = getValsKey(id, persistedProp, persistence);
var originalVal = extract(props[propName]);
var 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];
}
var vals = originalVal === undefined ? [newVal] : [newVal, originalVal];
storage.setItem(valsKey, vals, dispatch);
}
}
}, persisted_props);
}
/*
* Used for entire layouts (on load) or partial layouts (from children
* callbacks) to apply previously-stored UI edits to components
*/
function applyPersistence(layout, dispatch) {
if (Array.isArray(layout)) {
return layout.map(lay => (0, _wrapping.isDryComponent)(lay) ? persistenceMods(lay, lay, [], dispatch) : lay);
}
return persistenceMods(layout, layout, [], dispatch);
}
var UNDO = true;
function modProp(key, storage, element, props, persistedProp, update, undo) {
if (storage.hasItem(key)) {
var _storage$getItem = storage.getItem(key),
_storage$getItem2 = _slicedToArray(_storage$getItem, 2),
newVal = _storage$getItem2[0],
originalVal = _storage$getItem2[1];
var fromVal = undo ? newVal : originalVal;
var toVal = undo ? originalVal : newVal;
var _persistedProp$split3 = persistedProp.split('.'),
_persistedProp$split4 = _slicedToArray(_persistedProp$split3, 2),
propName = _persistedProp$split4[0],
propPart = _persistedProp$split4[1];
var transform = getTransform(element, propName, propPart);
if ((0, _ramda.equals)(fromVal, transform.extract(props[propName]))) {
update[propName] = transform.apply(toVal, propName in update ? update[propName] : props[propName]);
} else {
// clear this saved edit - we've started with the wrong
// value for this persistence ID
storage.removeItem(key);
}
}
}
function persistenceMods(layout, component, path, dispatch) {
var _getProps2 = getProps(component),
canPersist = _getProps2.canPersist,
id = _getProps2.id,
props = _getProps2.props,
element = _getProps2.element,
persistence = _getProps2.persistence,
persisted_props = _getProps2.persisted_props,
persistence_type = _getProps2.persistence_type;
var layoutOut = layout;
if (canPersist && persistence) {
var storage = getStore(persistence_type, dispatch);
var update = {};
(0, _ramda.forEach)(persistedProp => modProp(getValsKey(id, persistedProp, persistence), storage, element, props, persistedProp, update), persisted_props);
for (var propName in update) {
layoutOut = (0, _ramda.set)((0, _ramda.lensPath)(path.concat('props', propName)), update[propName], layoutOut);
}
}
// recurse inward
var children = props.children;
if (Array.isArray(children)) {
children.forEach((child, i) => {
if ((0, _ramda.type)(child) === 'Object' && child.props) {
layoutOut = persistenceMods(layoutOut, child, path.concat('props', 'children', i), dispatch);
}
});
} else if ((0, _ramda.type)(children) === 'Object' && children.props) {
layoutOut = persistenceMods(layoutOut, children, path.concat('props', 'children'), dispatch);
}
return layoutOut;
}
/*
* When we receive new explicit props from a callback,
* these override UI-driven edits of those exact props
* but not for props nested inside children
*/
function prunePersistence(layout, newProps, dispatch) {
var _getProps3 = getProps(layout),
canPersist = _getProps3.canPersist,
id = _getProps3.id,
props = _getProps3.props,
persistence = _getProps3.persistence,
persisted_props = _getProps3.persisted_props,
persistence_type = _getProps3.persistence_type,
element = _getProps3.element;
var getFinal = (propName, prevVal) => propName in newProps ? newProps[propName] : prevVal;
var finalPersistence = getFinal('persistence', persistence);
if (!canPersist || !(persistence || finalPersistence)) {
return newProps;
}
var finalPersistenceType = getFinal('persistence_type', persistence_type);
var finalPersistedProps = getFinal('persisted_props', persisted_props);
var persistenceChanged = finalPersistence !== persistence || finalPersistenceType !== persistence_type || finalPersistedProps !== persisted_props;
var notInNewProps = persistedProp => !(persistedProp.split('.')[0] in newProps);
var update = {};
var depersistedProps = props;
if (persistenceChanged && persistence) {
// clear previously-applied persistence
var storage = getStore(persistence_type, dispatch);
(0, _ramda.forEach)(persistedProp => modProp(getValsKey(id, persistedProp, persistence), storage, element, props, persistedProp, update, UNDO), (0, _ramda.filter)(notInNewProps, persisted_props));
depersistedProps = (0, _ramda.mergeRight)(props, update);
}
if (finalPersistence && persistenceChanged) {
var finalStorage = getStore(finalPersistenceType, dispatch);
// apply new persistence
(0, _ramda.forEach)(persistedProp => modProp(getValsKey(id, persistedProp, finalPersistence), finalStorage, element, depersistedProps, persistedProp, update), (0, _ramda.filter)(notInNewProps, finalPersistedProps));
}
return persistenceChanged ? (0, _ramda.mergeRight)(newProps, update) : newProps;
}