Remove child/parent state concept in persistedStateProvider (#15737)

* Remove child/parent state concept in persistedStateProvider

Looks like the only time it was used was when dashboard created child
ui states for visualize and saved searches.  We’re now all handling
this state passing via the embeddable layer.

* Remove more parent/child tests

* Remove extra spot that referenced the two removed parameters
This commit is contained in:
Stacey Gammon 2018-03-24 17:07:18 -04:00 committed by GitHub
parent 1968a1b1cf
commit 82901c8408
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 24 additions and 443 deletions

View file

@ -2,7 +2,6 @@ export function getContainerApiMock(config = {}) {
const containerApiMockDefaults = {
addFilter: () => {},
getAppState: () => {},
createChildUistate: () => {},
registerPanelIndexPattern: () => {},
updatePanel: () => {}
};

View file

@ -44,17 +44,6 @@ describe('Persisted State Provider', function () {
expect(persistedState.get()).to.not.equal(val);
});
it('should not throw if creating valid child object', function () {
const run = function () {
const val = { red: 'blue' };
const path = ['test.path'];
const parent = new PersistedState();
new PersistedState(val, path, parent);
};
expect(run).not.to.throwException();
});
it('should throw if given an invalid value', function () {
const run = function () {
const val = 'bananas';
@ -65,287 +54,6 @@ describe('Persisted State Provider', function () {
expect(err).to.be.a(PersistedStateError);
});
});
it('should not throw if given primitive to child', function () {
const run = function () {
const val = 'bananas';
const path = ['test.path'];
const parent = new PersistedState();
new PersistedState(val, path, parent);
};
expect(run).not.to.throwException();
});
it('should throw if given an invalid parent object', function () {
const run = function () {
const val = { red: 'blue' };
const path = ['test.path'];
const parent = {};
new PersistedState(val, path, parent);
};
expect(run).to.throwException(function (err) {
expect(err).to.be.a(PersistedStateError);
});
});
it('should throw if given a parent without a path', function () {
const run = function () {
const val = { red: 'blue' };
let path;
const parent = new PersistedState();
new PersistedState(val, path, parent);
};
expect(run).to.throwException(function (err) {
expect(err).to.be.a(PersistedStateError);
});
});
});
describe('child state creation', function () {
let childState;
it('should not append the child state to the parent, without parent value', function () {
const childIndex = 'i can haz child';
const persistedState = new PersistedState();
childState = persistedState.createChild(childIndex);
// parent state should not contain the child state
expect(persistedState.get()).to.not.have.property(childIndex);
expect(persistedState.get()).to.eql({});
});
it('should not append the child state to the parent, with parent value', function () {
const childIndex = 'i can haz child';
const persistedStateValue = { original: true };
const persistedState = new PersistedState(persistedStateValue);
childState = persistedState.createChild(childIndex);
// child state should be empty, we didn't give it any default data
expect(childState.get()).to.be(undefined);
// parent state should not contain the child state
expect(persistedState.get()).to.not.have.property(childIndex);
expect(persistedState.get()).to.eql(persistedStateValue);
});
it('should append the child state to the parent, with parent and child values', function () {
const childIndex = 'i can haz child';
const childStateValue = { tacos: 'yes please' };
const persistedStateValue = { original: true };
const persistedState = new PersistedState(persistedStateValue);
childState = persistedState.createChild(childIndex, childStateValue);
// parent state should contain the child and its original state value
const parentState = persistedState.get();
expect(parentState).to.have.property('original', true);
expect(parentState).to.have.property(childIndex);
expect(parentState[childIndex]).to.eql(childStateValue);
});
});
describe('deep child state creation', function () {
it('should delegate get/set calls to parent state', function () {
const children = [{
path: 'first*child',
value: { first: true, second: false }
}, {
path: 'second child',
value: { first: false, second: true }
}];
const persistedStateValue = { original: true };
const persistedState = new PersistedState(persistedStateValue);
// first child is a child of the parent persistedState
children[0].instance = persistedState.createChild(children[0].path, children[0].value);
// second child is a child of the first child
children[1].instance = children[0].instance.createChild(children[1].path, children[1].value);
// second child getter should only return second child value
expect(children[1].instance.get()).to.eql(children[1].value);
// parent should contain original props and first child path, but not the second child path
const parentState = persistedState.get();
_.keys(persistedStateValue).forEach(function (key) {
expect(parentState).to.have.property(key);
});
expect(parentState).to.have.property(children[0].path);
expect(parentState).to.not.have.property(children[1].path);
// second child path should be inside the first child
const firstChildState = children[0].instance.get();
expect(firstChildState).to.have.property(children[1].path);
expect(firstChildState[children[1].path]).to.eql(children[1].value);
// check that the second child is still accessible from the parent instance
const firstChild = persistedState.get(children[0].path);
expect(firstChild).to.have.property(children[1].path);
});
});
describe('child state removal', function () {
it('should clear path from parent state', function () {
const persistedState = new PersistedState();
persistedState.createChild('child', { userId: 1234 });
expect(persistedState.get()).to.eql({ child: { userId: 1234 } });
persistedState.removeChild('child');
expect(persistedState.get()).to.eql({});
});
it('should reset original parent value at path', function () {
const persistedState = new PersistedState({ user: 1234 });
persistedState.createChild('user', { id: 5678 });
expect(persistedState.get()).to.eql({ user: { id: 5678 } });
persistedState.removeChild('user');
expect(persistedState.get()).to.eql({ user: 1234 });
});
it('should clear changedState', function () {
const persistedState = new PersistedState({ user: 1234 });
const childState = persistedState.createChild('user');
childState.set('name', 'user name');
expect(persistedState.getChanges()).to.eql({ user: { name: 'user name' } });
persistedState.removeChild('user');
expect(persistedState.getChanges()).to.eql({});
});
});
describe('deep child state removal', function () {
it('should clear path from parent state', function () {
const persistedState = new PersistedState();
persistedState.createChild('child.state', { userId: 1234 });
expect(persistedState.get()).to.eql({ child: { state: { userId: 1234 } } });
persistedState.removeChild('child.state');
expect(persistedState.get()).to.eql({});
});
it('should reset original parent value at path', function () {
const persistedState = new PersistedState({ user: { id: 1234 } });
persistedState.createChild('user.id', 5678);
expect(persistedState.get()).to.eql({ user: { id: 5678 } });
persistedState.removeChild('user.id');
expect(persistedState.get()).to.eql({ user: { id: 1234 } });
});
it('should reset original parent other values at path', function () {
const persistedState = new PersistedState({ user: { name: 'user' } });
persistedState.createChild('user.id', 5678);
expect(persistedState.get()).to.eql({ user: { name: 'user', id: 5678 } });
persistedState.removeChild('user.id');
expect(persistedState.get()).to.eql({ user: { name: 'user' } });
});
it('should clear the changed state', function () {
const persistedState = new PersistedState({ user: { id: 1234 } });
const childState = persistedState.createChild('user.name');
childState.set('user name');
expect(persistedState.getChanges()).to.eql({ user: { name: 'user name' } });
persistedState.removeChild('user.name');
expect(persistedState.getChanges()).to.eql({});
});
});
describe('child state conditions', function () {
it('should be merged with the parent state', function () {
const parent = new PersistedState({ name: 'test' });
parent.createChild('child', 'value');
expect(parent.get()).to.eql({
name: 'test',
child: 'value'
});
parent.set('id', 1234);
expect(parent.get()).to.eql({
id: 1234,
name: 'test',
child: 'value'
});
parent.set({});
expect(parent.get()).to.eql({
child: 'value'
});
});
it('should give child state precedence', function () {
const parent = new PersistedState({ user: { id: 1234, name: 'test' } });
parent.createChild('user', { name: 'child test' });
expect(parent.get()).to.eql({
user: {
id: 1234,
name: 'child test'
}
});
parent.set({});
expect(parent.get()).to.eql({ user: { name: 'child test' } });
});
it('should be cleaned up with removeChild', function () {
const parent = new PersistedState({ name: 'test' });
parent.createChild('child', 'value');
expect(parent.get()).to.eql({
name: 'test',
child: 'value'
});
parent.removeChild('child');
expect(parent.get()).to.eql({
name: 'test'
});
});
});
describe('colliding child paths and parent state values', function () {
it('should not change the child path value by default', function () {
const childIndex = 'childTest';
const persistedStateValue = {};
persistedStateValue[childIndex] = { overlapping_index: true };
const persistedState = new PersistedState(persistedStateValue);
let state = persistedState.get();
expect(state).to.have.property(childIndex);
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
const childState = persistedState.createChild(childIndex);
expect(childState.get()).to.eql(persistedStateValue[childIndex]);
// make sure the parent state is still the same
state = persistedState.get();
expect(state).to.have.property(childIndex);
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
});
it('should merge default child state', function () {
const childIndex = 'childTest';
const childStateValue = { child_index: false };
const persistedStateValue = {};
persistedStateValue[childIndex] = { parent_index: true };
const persistedState = new PersistedState(persistedStateValue);
let state = persistedState.get();
expect(state).to.have.property(childIndex);
expect(state[childIndex]).to.eql(persistedStateValue[childIndex]);
// pass in child state value
const childState = persistedState.createChild(childIndex, childStateValue);
// parent's default state is merged with child state
const compare = _.merge({}, childStateValue, persistedStateValue[childIndex]);
expect(childState.get()).to.eql(compare);
state = persistedState.get();
expect(state).to.have.property(childIndex);
expect(state[childIndex]).to.eql(compare);
});
});
describe('mutation', function () {
@ -376,27 +84,6 @@ describe('Persisted State Provider', function () {
const json = persistedState.toJSON();
expect(json).to.eql(persistedStateValue);
});
it('should return the JSON representation of the child state', function () {
const persistedState = new PersistedState(persistedStateValue);
const childState = persistedState.createChild('awesome', { pants: false });
expect(childState.toJSON()).to.eql({ pants: false });
// verify JSON output of the parent state
const parentCompare = _.assign({ awesome: { pants: false } }, persistedStateValue);
expect(persistedState.toJSON()).to.eql(parentCompare);
});
it('should export stringified version of state', function () {
const persistedState = new PersistedState(persistedStateValue);
const childState = persistedState.createChild('awesome', { pants: false });
const data = childState.toString();
expect(JSON.parse(data)).to.eql({ pants: false });
// verify JSON output of the parent state
const parentCompare = _.assign({ awesome: { pants: false } }, persistedStateValue);
expect(JSON.parse(persistedState.toString())).to.eql(parentCompare);
});
});
describe('importing state from JSON string (hydration)', function () {
@ -449,15 +136,6 @@ describe('Persisted State Provider', function () {
expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'humans', 'everyone'] });
expect(persistedState.get('hello.nouns')).to.eql(['world', 'humans', 'everyone']);
});
it('should pass defaults to parent delegation', function () {
const persistedState = new PersistedState({ parent: true });
const childState = persistedState.createChild('child', { account: { name: 'first child' } });
const defaultValue = 'i have no data';
expect(childState.get('account.name', defaultValue)).to.eql('first child');
expect(childState.get('account.age', defaultValue)).to.eql(defaultValue);
});
});
describe('set state', function () {
@ -625,51 +303,5 @@ describe('Persisted State Provider', function () {
persistedState.set('checker.events', 'i changed');
expect(getByType('change')).to.have.length(1);
});
it('should not emit change when createChild has no value', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker');
expect(getByType('change')).to.have.length(0);
});
it('should not emit change when createChild is same value', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker', { events: 'event tests' });
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker.events', 'event tests');
expect(getByType('change')).to.have.length(0);
});
it('should emit change when createChild changes existing value', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker', { events: 'changed via child' });
expect(getByType('change')).to.have.length(1);
});
it('should not emit when createChild set to silent', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('checker', { events: 'changed via child' }, true);
expect(getByType('change')).to.have.length(0);
});
it('should emit change when createChild adds new value', function () {
expect(getByType('change')).to.have.length(0);
persistedState.createChild('new.path', { another: 'thing' });
expect(getByType('change')).to.have.length(1);
});
it('should emit on parent and child instances', function (done) {
const child = persistedState.createChild('checker');
expect(getByType('change')).to.have.length(0);
// parent and child should emit, set up listener to test
child.on('change', function () {
// child fired, make sure parent fires as well
expect(getByType('change')).to.have.length(1);
done();
});
child.set('events', 'changed via child set');
});
});
});

View file

@ -25,8 +25,8 @@ module.factory('PersistedState', ($injector) => {
// Extend PersistedState to override the EmitterClass class with
// our Angular friendly version.
return class AngularPersistedState extends PersistedState {
constructor(value, path, parent, silent) {
super(value, path, parent, silent, Events);
constructor(value, path) {
super(value, path, Events);
}
};
});

View file

@ -30,43 +30,32 @@ export class PersistedState {
*
* @param value
* @param path
* @param parent
* @param silent
* @param EmitterClass {SimpleEmitter} - a SimpleEmitter class that this class will extend. Can be used to
* inherit a custom event emitter. For example, the EventEmitter is an "angular-ized" version
* for angular components which automatically triggers a digest loop for every registered
* handler. TODO: Get rid of the need for EventEmitter by wrapping handlers that require it
* in a special function that will handler triggering the digest loop.
* handler. TODO: replace angularized SimpleEmitter and force angular callers to handle digest loops manually ala
* https://github.com/elastic/kibana/issues/13855
*/
constructor(value, path, parent, silent, EmitterClass = SimpleEmitter) {
constructor(value, path, EmitterClass = SimpleEmitter) {
EmitterClass.call(this);
this._EmitterClass = EmitterClass;
this._path = this._setPath(path);
this._parent = parent || false;
_.forOwn(EmitterClass.prototype, (method, methodName) => {
this[methodName] = function () {
return EmitterClass.prototype[methodName].apply(this._parent || this, arguments);
return EmitterClass.prototype[methodName].apply(this, arguments);
};
});
// Some validations
if (this._parent) {
if (this._path.length <= 0) {
throw new PersistedStateError('PersistedState child objects must contain a path');
}
if (!(this._parent instanceof PersistedState)) {
throw new PersistedStateError('Parent object must be an instance of PersistedState');
}
} else if (!this._path.length && value && !_.isPlainObject(value)) {
if (!this._path.length && value && !_.isPlainObject(value)) {
throw new PersistedStateError('State value must be a plain object');
}
value = value || this._getDefault();
// copy passed state values and create internal trackers
(silent) ? this.setSilent(value) : this.set(value);
this.set(value);
this._initialized = true; // used to track state changes
}
@ -103,35 +92,12 @@ export class PersistedState {
_.set(this._mergedState, keyPath, origValue);
}
// clean up the changedState and defaultChildState trees
// clean up the changedState tree
this._cleanPath(path, this._changedState);
this._cleanPath(path, this._defaultChildState);
if (!_.isEqual(currentValue, origValue)) this.emit('change');
}
/**
*
* @param path {String}
* @param value {Object} The uiState to store.
* @param silent {Boolean}
* @returns {PersistedState}
*/
createChild(path, value, silent) {
this._setChild(this._getIndex(path), value, this._parent || this);
return new PersistedState(value, this._getIndex(path), this._parent || this, silent, this._EmitterClass);
}
removeChild(path) {
const origValue = _.get(this._defaultState, this._getIndex(path));
if (_.isUndefined(origValue)) {
this.reset(path);
} else {
this.set(path, origValue);
}
}
getChanges() {
return _.cloneDeep(this._changedState);
}
@ -177,8 +143,7 @@ export class PersistedState {
}
_getDefault() {
const def = (this._hasPath()) ? undefined : {};
return (this._parent ? this.get() : def);
return this._hasPath() ? undefined : {};
}
_setPath(path) {
@ -189,19 +154,11 @@ export class PersistedState {
return (isString) ? [this._getIndex(path)] : path;
}
_setChild(path, value, parent) {
parent._defaultChildState = parent._defaultChildState || {};
_.set(parent._defaultChildState, path, value);
}
_hasPath() {
return this._path.length > 0;
}
_get(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;
@ -210,7 +167,7 @@ export class PersistedState {
return _.get(this._mergedState, this._getIndex(key), def);
}
_set(key, value, silent, initialChildState) {
_set(key, value, silent) {
const self = this;
let stateChanged = false;
const initialState = !this._initialized;
@ -224,47 +181,40 @@ export class PersistedState {
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);
}
this._changedState = value;
this._mergedState = _.cloneDeep(value);
} else {
// check for changes at path, emit an event when different
const 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 (Array.isArray(_.get(this._mergedState, keyPath))) {
if (hasKeyPath) _.set(this._mergedState, keyPath, undefined);
else this._mergedState = undefined;
}
// arrays are merge by index, not desired - ensure they are replaced
if (Array.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 : {};
if (hasKeyPath) {
_.set(this._changedState, keyPath, value);
} else {
this._changedState = _.isPlainObject(value) ? value : {};
}
}
}
// update the merged state value
const targetObj = this._mergedState || _.cloneDeep(this._defaultState);
const sourceObj = _.merge({}, this._defaultChildState, this._changedState);
const sourceObj = _.merge({}, this._changedState);
// handler arguments are (targetValue, sourceValue, key, target, source)
const 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))) {
if (!initialState && _.isEqual(keyPath, self._getIndex(mergeKey))) {
// use the sourceValue or fall back to targetValue
return !_.isUndefined(sourceValue) ? sourceValue : targetValue;
}