UNPKG

dash-renderer

Version:

render dash components in react

424 lines (415 loc) 17.9 kB
"use strict"; 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; }