kibana-123
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
279 lines (223 loc) • 9.24 kB
JavaScript
/**
* @name PersistedState
*
* @extends Events
*/
import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import errors from 'ui/errors';
import SimpleEmitter from 'ui/utils/simple_emitter';
import EventsProvider from 'ui/events';
export default function (Private) {
let Events = Private(EventsProvider);
function validateParent(parent, path) {
if (path.length <= 0) {
throw new errors.PersistedStateError('PersistedState child objects must contain a path');
}
if (parent instanceof PersistedState) return;
throw new errors.PersistedStateError('Parent object must be an instance of PersistedState');
}
function validateValue(value) {
let msg = 'State value must be a plain object';
if (!value) return;
if (!_.isPlainObject(value)) throw new errors.PersistedStateError(msg);
}
function prepSetParams(key, value, path) {
// key must be the value, set the entire state using it
if (_.isUndefined(value) && (_.isPlainObject(key) || path.length > 0)) {
// setting entire tree, swap the key and value to write to the state
value = key;
key = undefined;
}
// ensure the value being passed in is never mutated
return {
value: _.cloneDeep(value),
key: key
};
}
function parentDelegationMixin(from, to) {
_.forOwn(from.prototype, function (method, methodName) {
to.prototype[methodName] = function () {
return from.prototype[methodName].apply(this._parent || this, arguments);
};
});
}
_.class(PersistedState).inherits(Events);
parentDelegationMixin(SimpleEmitter, PersistedState);
parentDelegationMixin(Events, PersistedState);
function PersistedState(value, path, parent, silent) {
PersistedState.Super.call(this);
this._path = this._setPath(path);
this._parent = parent || false;
if (this._parent) {
validateParent(this._parent, this._path);
} else if (!this._path.length) {
validateValue(value);
}
value = value || this._getDefault();
// copy passed state values and create internal trackers
(silent) ? this.setSilent(value) : this.set(value);
this._initialized = true; // used to track state changes
}
PersistedState.prototype.get = function (key, def) {
return _.cloneDeep(this._get(key, def));
};
PersistedState.prototype.set = function (key, value) {
let params = prepSetParams(key, value, this._path);
let val = this._set(params.key, params.value);
this.emit('set');
return val;
};
PersistedState.prototype.setSilent = function (key, value) {
let params = prepSetParams(key, value, this._path);
return this._set(params.key, params.value, true);
};
PersistedState.prototype.reset = function (path) {
let keyPath = this._getIndex(path);
let origValue = _.get(this._defaultState, keyPath);
let currentValue = _.get(this._mergedState, keyPath);
if (_.isUndefined(origValue)) {
this._cleanPath(path, this._mergedState);
} else {
_.set(this._mergedState, keyPath, origValue);
}
// clean up the changedState and defaultChildState trees
this._cleanPath(path, this._changedState);
this._cleanPath(path, this._defaultChildState);
if (!_.isEqual(currentValue, origValue)) this.emit('change');
};
PersistedState.prototype.createChild = function (path, value, silent) {
this._setChild(this._getIndex(path), value, this._parent || this);
return new PersistedState(value, this._getIndex(path), this._parent || this, silent);
};
PersistedState.prototype.removeChild = function (path) {
let origValue = _.get(this._defaultState, this._getIndex(path));
if (_.isUndefined(origValue)) {
this.reset(path);
} else {
this.set(path, origValue);
}
};
PersistedState.prototype.getChanges = function () {
return _.cloneDeep(this._changedState);
};
PersistedState.prototype.toJSON = function () {
return this.get();
};
PersistedState.prototype.toString = function () {
return JSON.stringify(this.toJSON());
};
PersistedState.prototype.fromString = function (input) {
return this.set(JSON.parse(input));
};
PersistedState.prototype._getIndex = function (key) {
if (_.isUndefined(key)) return this._path;
return (this._path || []).concat(toPath(key));
};
PersistedState.prototype._getPartialIndex = function (key) {
let keyPath = this._getIndex(key);
return keyPath.slice(this._path.length);
};
PersistedState.prototype._cleanPath = function (path, stateTree) {
let partialPath = this._getPartialIndex(path);
let remove = true;
// recursively delete value tree, when no other keys exist
while (partialPath.length > 0) {
let lastKey = partialPath.splice(partialPath.length - 1, 1)[0];
let statePath = this._path.concat(partialPath);
let stateVal = statePath.length > 0 ? _.get(stateTree, statePath) : stateTree;
// if stateVal isn't an object, do nothing
if (!_.isPlainObject(stateVal)) return;
if (remove) delete stateVal[lastKey];
if (Object.keys(stateVal).length > 0) remove = false;
}
};
PersistedState.prototype._getDefault = function () {
let def = (this._hasPath()) ? undefined : {};
return (this._parent ? this.get() : def);
};
PersistedState.prototype._setPath = function (path) {
let isString = _.isString(path);
let isArray = _.isArray(path);
if (!isString && !isArray) return [];
return (isString) ? [this._getIndex(path)] : path;
};
PersistedState.prototype._setChild = function (path, value, parent) {
parent._defaultChildState = parent._defaultChildState || {};
_.set(parent._defaultChildState, path, value);
};
PersistedState.prototype._hasPath = function () {
return this._path.length > 0;
};
PersistedState.prototype._get = function (key, def) {
// delegate to parent instance
if (this._parent) return this._parent._get(this._getIndex(key), def);
// no path and no key, get the whole state
if (!this._hasPath() && _.isUndefined(key)) {
return this._mergedState;
}
return _.get(this._mergedState, this._getIndex(key), def);
};
PersistedState.prototype._set = function (key, value, silent, initialChildState) {
let self = this;
let stateChanged = false;
let initialState = !this._initialized;
let keyPath = this._getIndex(key);
let hasKeyPath = keyPath.length > 0;
// if this is the initial state value, save value as the default
if (initialState) {
this._changedState = {};
if (!this._hasPath() && _.isUndefined(key)) this._defaultState = value;
else this._defaultState = _.set({}, keyPath, value);
}
// delegate to parent instance, passing child's default value
if (this._parent) {
return this._parent._set(keyPath, value, silent, initialState);
}
// everything in here affects only the parent state
if (!initialState) {
// no path and no key, set the whole state
if (!this._hasPath() && _.isUndefined(key)) {
// compare changedState and new state, emit an event when different
stateChanged = !_.isEqual(this._changedState, value);
if (!initialChildState) {
this._changedState = value;
this._mergedState = _.cloneDeep(value);
}
} else {
// check for changes at path, emit an event when different
let curVal = hasKeyPath ? this.get(keyPath) : this._mergedState;
stateChanged = !_.isEqual(curVal, value);
if (!initialChildState) {
// arrays are merge by index, not desired - ensure they are replaced
if (_.isArray(_.get(this._mergedState, keyPath))) {
if (hasKeyPath) _.set(this._mergedState, keyPath, undefined);
else this._mergedState = undefined;
}
if (hasKeyPath) _.set(this._changedState, keyPath, value);
else this._changedState = _.isPlainObject(value) ? value : {};
}
}
}
// update the merged state value
let targetObj = this._mergedState || _.cloneDeep(this._defaultState);
let sourceObj = _.merge({}, this._defaultChildState, this._changedState);
// handler arguments are (targetValue, sourceValue, key, target, source)
let mergeMethod = function (targetValue, sourceValue, mergeKey) {
// if not initial state, skip default merge method (ie. return value, see note below)
if (!initialState && !initialChildState && _.isEqual(keyPath, self._getIndex(mergeKey))) {
// use the sourceValue or fall back to targetValue
return !_.isUndefined(sourceValue) ? sourceValue : targetValue;
}
};
// If `mergeMethod` is provided it is invoked to produce the merged values of the
// destination and source properties.
// If `mergeMethod` returns `undefined` the default merging method is used
this._mergedState = _.merge(targetObj, sourceObj, mergeMethod);
// sanity check; verify that there are actually changes
if (_.isEqual(this._mergedState, this._defaultState)) this._changedState = {};
if (!silent && stateChanged) this.emit('change');
return this;
};
return PersistedState;
};