From 6d23892f9a7ae7daf2d26921f578d8338f44fe7d Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 27 Feb 2020 13:57:23 +0300 Subject: [PATCH] =?UTF-8?q?[Step=201]=20ui/persisted=5Fstate=20?= =?UTF-8?q?=F0=9F=91=89=20src/plugins/visualizations=20(#58324)=20(#58698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui/persisted_state 👉 src/plugins/visualizations Part of #46926 * fix JEST * remove sinon from tests Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../public/discover/get_inner_angular.ts | 19 - .../public/visualize/np_ready/application.ts | 18 - .../public/visualize/np_ready/types.d.ts | 2 +- .../__tests__/persisted_state_provider.js | 328 ------------------ src/legacy/ui/public/persisted_state/index.js | 3 +- .../persisted_state.factory.js | 51 --- .../persisted_state_provider.test.ts | 306 ++++++++++++++++ .../ui/public/state_management/app_state.js | 6 +- .../public/np_imports/angular/modules.ts | 18 - 9 files changed, 311 insertions(+), 440 deletions(-) delete mode 100644 src/legacy/ui/public/persisted_state/__tests__/persisted_state_provider.js delete mode 100644 src/legacy/ui/public/persisted_state/persisted_state.factory.js create mode 100644 src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index bae938d1fb61e..9c29e182c55d6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -24,9 +24,6 @@ import angular from 'angular'; import { EuiIcon } from '@elastic/eui'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; -// @ts-ignore -import { EventsProvider } from 'ui/events'; -import { PersistedState } from 'ui/persisted_state'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; // @ts-ignore @@ -119,7 +116,6 @@ export function initializeInnerAngularModule( createLocalPromiseModule(); createLocalConfigModule(core.uiSettings); createLocalKbnUrlModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(navigation); createLocalGlobalStateModule(); createLocalAppStateModule(); @@ -140,7 +136,6 @@ export function initializeInnerAngularModule( 'discoverPrivate', 'discoverDocTable', 'discoverPagerFactory', - 'discoverPersistedState', ]) .config(watchMultiDecorator) .directive('icon', reactDirective => reactDirective(EuiIcon)) @@ -158,7 +153,6 @@ export function initializeInnerAngularModule( 'discoverConfig', 'discoverI18n', 'discoverPrivate', - 'discoverPersistedState', 'discoverTopNav', 'discoverGlobalState', 'discoverAppState', @@ -197,19 +191,6 @@ export function createLocalGlobalStateModule() { }); } -function createLocalPersistedStateModule() { - angular - .module('discoverPersistedState', ['discoverPrivate', 'discoverPromise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; - }); -} - function createLocalKbnUrlModule() { angular .module('discoverKbnUrl', ['discoverPrivate', 'ngRoute']) diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 0e1abff4b46f2..b15d89275eba7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -23,12 +23,10 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { configureAppAngularModule, - EventsProvider, GlobalStateProvider, KbnUrlProvider, RedirectWhenMissingProvider, IPrivate, - PersistedState, PrivateProvider, PromiseServiceCreator, StateManagementConfigProvider, @@ -92,7 +90,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalConfigModule(core); createLocalKbnUrlModule(); createLocalStateModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(navigation); const visualizeAngularModule: IModule = angular.module(moduleName, [ @@ -100,7 +97,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav 'app/visualize/Config', 'app/visualize/I18n', 'app/visualize/Private', - 'app/visualize/PersistedState', 'app/visualize/TopNav', 'app/visualize/State', ]); @@ -114,26 +110,12 @@ function createLocalStateModule() { 'app/visualize/Config', 'app/visualize/KbnUrl', 'app/visualize/Promise', - 'app/visualize/PersistedState', ]) .service('globalState', function(Private: IPrivate) { return Private(GlobalStateProvider); }); } -function createLocalPersistedStateModule() { - angular - .module('app/visualize/PersistedState', ['app/visualize/Private', 'app/visualize/Promise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; - }); -} - function createLocalKbnUrlModule() { angular .module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute']) diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 139c247aa29cc..3b6ecb45b83b3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -20,7 +20,7 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; -import { VisState, Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { VisSavedObject, PersistedState } from '../legacy_imports'; export type PureVisState = ReturnType; diff --git a/src/legacy/ui/public/persisted_state/__tests__/persisted_state_provider.js b/src/legacy/ui/public/persisted_state/__tests__/persisted_state_provider.js deleted file mode 100644 index 8f89726d15955..0000000000000 --- a/src/legacy/ui/public/persisted_state/__tests__/persisted_state_provider.js +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import sinon from 'sinon'; -import noDigestPromises from 'test_utils/no_digest_promises'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { PersistedStateError } from '../errors'; -import '..'; - -let PersistedState; - -describe('Persisted State Provider', function() { - noDigestPromises.activateForSuite(); - - beforeEach(function() { - ngMock.module('kibana'); - - ngMock.inject(function($injector) { - PersistedState = $injector.get('PersistedState'); - }); - }); - - describe('state creation', function() { - let persistedState; - - it('should create an empty state instance', function() { - persistedState = new PersistedState(); - expect(persistedState.get()).to.eql({}); - }); - - it('should create a state instance with data', function() { - const val = { red: 'blue' }; - persistedState = new PersistedState(val); - - expect(persistedState.get()).to.eql(val); - // ensure we get a copy of the state, not the actual state object - expect(persistedState.get()).to.not.equal(val); - }); - - it('should create a copy of the state passed in', function() { - const val = { red: 'blue' }; - persistedState = new PersistedState(val); - - expect(persistedState.get()).to.eql(val); - expect(persistedState.get()).to.not.equal(val); - }); - - it('should throw if given an invalid value', function() { - const run = function() { - const val = 'bananas'; - new PersistedState(val); - }; - - expect(run).to.throwException(function(err) { - expect(err).to.be.a(PersistedStateError); - }); - }); - }); - - describe('mutation', function() { - it('should not mutate the internal object', function() { - const persistedStateValue = { hello: 'world' }; - const insertedObj = { farewell: 'cruel world' }; - const persistedState = new PersistedState(persistedStateValue); - - const obj = persistedState.get(); - _.assign(obj, insertedObj); - - expect(obj).to.have.property('farewell'); - expect(persistedState.get()).to.not.have.property('farewell'); - }); - }); - - describe('JSON importing and exporting', function() { - let persistedStateValue; - - beforeEach(function() { - persistedStateValue = { one: 1, two: 2, 'meaning of life': 42 }; - }); - - describe('exporting state to JSON', function() { - it('should return the full JSON representation', function() { - const persistedState = new PersistedState(persistedStateValue); - - const json = persistedState.toJSON(); - expect(json).to.eql(persistedStateValue); - }); - }); - - describe('importing state from JSON string (hydration)', function() { - it('should set the state from JSON string input', function() { - const stateJSON = JSON.stringify(persistedStateValue); - const persistedState = new PersistedState(); - expect(persistedState.get()).to.eql({}); - - persistedState.fromString(stateJSON); - expect(persistedState.get()).to.eql(persistedStateValue); - }); - }); - }); - - describe('get state', function() { - it('should perform deep gets with various formats', function() { - const obj = { - red: { - green: { - blue: 'yellow', - }, - }, - orange: [1, 2, false, 4], - purple: { - violet: '', - }, - }; - const persistedState = new PersistedState(obj); - expect(persistedState.get()).to.eql(obj); - - expect(persistedState.get('red')).to.eql({ green: { blue: 'yellow' } }); - expect(persistedState.get('red.green')).to.eql({ blue: 'yellow' }); - expect(persistedState.get('red[green]')).to.eql({ blue: 'yellow' }); - expect(persistedState.get(['red', 'green'])).to.eql({ blue: 'yellow' }); - expect(persistedState.get('red.green.blue')).to.eql('yellow'); - expect(persistedState.get('red[green].blue')).to.eql('yellow'); - expect(persistedState.get('red.green[blue]')).to.eql('yellow'); - expect(persistedState.get('red[green][blue]')).to.eql('yellow'); - expect(persistedState.get('red.green.blue')).to.eql('yellow'); - expect(persistedState.get('orange')).to.eql([1, 2, false, 4]); - expect(persistedState.get('orange[0]')).to.equal(1); - expect(persistedState.get('orange[2]')).to.equal(false); - expect(persistedState.get('purple')).to.eql({ violet: '' }); - }); - - it('should perform deep gets with arrays', function() { - const persistedState = new PersistedState({ - hello: { nouns: ['world', 'humans', 'everyone'] }, - }); - - expect(persistedState.get()).to.eql({ hello: { nouns: ['world', 'humans', 'everyone'] } }); - expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'humans', 'everyone'] }); - expect(persistedState.get('hello.nouns')).to.eql(['world', 'humans', 'everyone']); - }); - }); - - describe('set state', function() { - describe('path format support', function() { - it('should create deep objects from dot notation', function() { - const persistedState = new PersistedState(); - persistedState.set('one.two.three', 4); - expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } }); - }); - - it('should create deep objects from array notation', function() { - const persistedState = new PersistedState(); - persistedState.set('one[two][three]', 4); - expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } }); - }); - - it('should create deep objects from arrays', function() { - const persistedState = new PersistedState(); - persistedState.set(['one', 'two', 'three'], 4); - expect(persistedState.get()).to.eql({ one: { two: { three: 4 } } }); - }); - - it('should create deep objects with an existing path', function() { - const persistedState = new PersistedState({}, 'deep.path'); - persistedState.set('green[red].blue', 4); - expect(persistedState.get()).to.eql({ green: { red: { blue: 4 } } }); - }); - }); - - describe('simple replace operations', function() { - let persistedState; - - it('should replace value with string', function() { - persistedState = new PersistedState({ hello: 'world' }); - expect(persistedState.get()).to.eql({ hello: 'world' }); - - persistedState.set('hello', 'fare thee well'); - expect(persistedState.get()).to.eql({ hello: 'fare thee well' }); - }); - - it('should replace value with array', function() { - persistedState = new PersistedState({ hello: ['world', 'everyone'] }); - expect(persistedState.get()).to.eql({ hello: ['world', 'everyone'] }); - - persistedState.set('hello', ['people']); - expect(persistedState.get()).to.eql({ hello: ['people'] }); - }); - - it('should replace value with object', function() { - persistedState = new PersistedState({ hello: 'world' }); - expect(persistedState.get()).to.eql({ hello: 'world' }); - - persistedState.set('hello', { message: 'fare thee well' }); - expect(persistedState.get()).to.eql({ hello: { message: 'fare thee well' } }); - }); - - it('should replace value with object, removing old properties', function() { - persistedState = new PersistedState({ hello: { message: 'world' } }); - expect(persistedState.get()).to.eql({ hello: { message: 'world' } }); - - persistedState.set('hello', { length: 5 }); - expect(persistedState.get()).to.eql({ hello: { length: 5 } }); - }); - }); - - describe('deep replace operations', function() { - let persistedState; - - it('should append to the object', function() { - persistedState = new PersistedState({ hello: { message: 'world' } }); - expect(persistedState.get()).to.eql({ hello: { message: 'world' } }); - - persistedState.set('hello.length', 5); - expect(persistedState.get()).to.eql({ hello: { message: 'world', length: 5 } }); - }); - - it('should change the value in the array', function() { - persistedState = new PersistedState({ hello: { nouns: ['world', 'humans', 'everyone'] } }); - persistedState.set('hello.nouns[1]', 'aliens'); - - expect(persistedState.get()).to.eql({ hello: { nouns: ['world', 'aliens', 'everyone'] } }); - expect(persistedState.get('hello')).to.eql({ nouns: ['world', 'aliens', 'everyone'] }); - expect(persistedState.get('hello.nouns')).to.eql(['world', 'aliens', 'everyone']); - }); - }); - }); - - describe('internal state tracking', function() { - it('should be an empty object', function() { - const persistedState = new PersistedState(); - expect(persistedState._defaultState).to.eql({}); - }); - - it('should store the default state value', function() { - const val = { one: 1, two: 2 }; - const persistedState = new PersistedState(val); - expect(persistedState._defaultState).to.eql(val); - }); - - it('should keep track of changes', function() { - const val = { one: 1, two: 2 }; - const persistedState = new PersistedState(val); - - persistedState.set('two', 22); - expect(persistedState._defaultState).to.eql(val); - expect(persistedState._changedState).to.eql({ two: 22 }); - }); - }); - - describe('events', function() { - let persistedState; - let emitter; - - const getByType = function(type, spy) { - spy = spy || emitter; - return spy.getCalls().filter(function(call) { - return call.args[0] === type; - }); - }; - - const watchEmitter = function(state) { - return sinon.spy(state, 'emit'); - }; - - beforeEach(function() { - persistedState = new PersistedState({ checker: { events: 'event tests' } }); - emitter = watchEmitter(persistedState); - }); - - it('should emit set when setting values', function() { - expect(getByType('set')).to.have.length(0); - persistedState.set('checker.time', 'now'); - expect(getByType('set')).to.have.length(1); - }); - - it('should not emit when setting value silently', function() { - expect(getByType('set')).to.have.length(0); - persistedState.setSilent('checker.time', 'now'); - expect(getByType('set')).to.have.length(0); - }); - - it('should emit change when changing values', function() { - expect(getByType('change')).to.have.length(0); - persistedState.set('checker.time', 'now'); - expect(getByType('change')).to.have.length(1); - }); - - it('should not emit when changing values silently', function() { - expect(getByType('change')).to.have.length(0); - persistedState.setSilent('checker.time', 'now'); - expect(getByType('change')).to.have.length(0); - }); - - it('should not emit change when values are identical', function() { - expect(getByType('change')).to.have.length(0); - // check both forms of setting the same value - persistedState.set('checker', { events: 'event tests' }); - expect(getByType('change')).to.have.length(0); - persistedState.set('checker.events', 'event tests'); - expect(getByType('change')).to.have.length(0); - }); - - it('should emit change when values change', function() { - expect(getByType('change')).to.have.length(0); - persistedState.set('checker.events', 'i changed'); - expect(getByType('change')).to.have.length(1); - }); - }); -}); diff --git a/src/legacy/ui/public/persisted_state/index.js b/src/legacy/ui/public/persisted_state/index.js index 4b6ce7ce68dce..ab5a3e7be7d28 100644 --- a/src/legacy/ui/public/persisted_state/index.js +++ b/src/legacy/ui/public/persisted_state/index.js @@ -17,5 +17,4 @@ * under the License. */ -import './persisted_state.factory.js'; -export { PersistedState } from './persisted_state.js'; +export { PersistedState } from './persisted_state'; diff --git a/src/legacy/ui/public/persisted_state/persisted_state.factory.js b/src/legacy/ui/public/persisted_state/persisted_state.factory.js deleted file mode 100644 index ff16d2d90110d..0000000000000 --- a/src/legacy/ui/public/persisted_state/persisted_state.factory.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * @name AngularPersistedState - * - * Returns a PersistedState object which uses an EventEmitter instead of - * the SimpleEmitter. The EventEmitter adds digest loops every time a handler is called - * so it's preferable to use this variation when a callback modifies angular UI. - * - * TODO: The handlers themselves should really be responsible for triggering digest loops - * as opposed to having an all or nothing situation. A nice goal would be to get rid - * of the EventEmitter entirely and require handlers that need it to trigger a digest loop - * themselves. We can even supply a service to wrap the callbacks in a function that - * would call the callback, and finish with a $rootScope.$apply(). - */ - -import { EventsProvider } from '../events'; -import { PersistedState } from './persisted_state'; -import { uiModules } from '../modules'; - -const module = uiModules.get('kibana'); - -module.factory('PersistedState', $injector => { - const Private = $injector.get('Private'); - const Events = Private(EventsProvider); - - // Extend PersistedState to override the EmitterClass class with - // our Angular friendly version. - return class AngularPersistedState extends PersistedState { - constructor(value, path) { - super(value, path, Events); - } - }; -}); diff --git a/src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts b/src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts new file mode 100644 index 0000000000000..f14215002cf8f --- /dev/null +++ b/src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts @@ -0,0 +1,306 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PersistedStateError } from './errors'; +import { PersistedState } from './persisted_state'; + +describe('Persisted State Provider', () => { + describe('state creation', () => { + let persistedState: PersistedState; + + test('should create an empty state instance', () => { + persistedState = new PersistedState(); + expect(persistedState.get()).toEqual({}); + }); + + test('should create a state instance with data', () => { + const val = { red: 'blue' }; + persistedState = new PersistedState(val); + + expect(persistedState.get()).toEqual(val); + // ensure we get a copy of the state, not the actual state object + expect(persistedState.get()).not.toBe(val); + }); + + test('should create a copy of the state passed in', () => { + const val = { red: 'blue' }; + persistedState = new PersistedState(val); + + expect(persistedState.get()).toEqual(val); + expect(persistedState.get()).not.toBe(val); + }); + + test('should throw if given an invalid value', () => { + expect(() => new PersistedState('bananas')).toThrow(PersistedStateError); + }); + }); + + describe('mutation', () => { + test('should not mutate the internal object', () => { + const persistedStateValue = { hello: 'world' }; + const insertedObj = { farewell: 'cruel world' }; + const persistedState = new PersistedState(persistedStateValue); + + expect({ + ...persistedState.get(), + ...insertedObj, + }).toHaveProperty('farewell'); + + expect(persistedState.get()).not.toHaveProperty('farewell'); + }); + }); + + describe('JSON importing and exporting', () => { + let persistedStateValue: any; + + beforeEach(() => { + persistedStateValue = { one: 1, two: 2, 'meaning of life': 42 }; + }); + + describe('exporting state to JSON', () => { + test('should return the full JSON representation', () => { + const persistedState = new PersistedState(persistedStateValue); + + const json = persistedState.toJSON(); + expect(json).toEqual(persistedStateValue); + }); + }); + + describe('importing state from JSON string (hydration)', () => { + test('should set the state from JSON string input', () => { + const stateJSON = JSON.stringify(persistedStateValue); + const persistedState = new PersistedState(); + expect(persistedState.get()).toEqual({}); + + persistedState.fromString(stateJSON); + expect(persistedState.get()).toEqual(persistedStateValue); + }); + }); + }); + + describe('get state', () => { + test('should perform deep gets with various formats', () => { + const obj = { + red: { + green: { + blue: 'yellow', + }, + }, + orange: [1, 2, false, 4], + purple: { + violet: '', + }, + }; + const persistedState = new PersistedState(obj); + expect(persistedState.get()).toEqual(obj); + + expect(persistedState.get('red')).toEqual({ green: { blue: 'yellow' } }); + expect(persistedState.get('red.green')).toEqual({ blue: 'yellow' }); + expect(persistedState.get('red[green]')).toEqual({ blue: 'yellow' }); + expect(persistedState.get(['red', 'green'])).toEqual({ blue: 'yellow' }); + expect(persistedState.get('red.green.blue')).toEqual('yellow'); + expect(persistedState.get('red[green].blue')).toEqual('yellow'); + expect(persistedState.get('red.green[blue]')).toEqual('yellow'); + expect(persistedState.get('red[green][blue]')).toEqual('yellow'); + expect(persistedState.get('red.green.blue')).toEqual('yellow'); + expect(persistedState.get('orange')).toEqual([1, 2, false, 4]); + expect(persistedState.get('orange[0]')).toEqual(1); + expect(persistedState.get('orange[2]')).toEqual(false); + expect(persistedState.get('purple')).toEqual({ violet: '' }); + }); + + test('should perform deep gets with arrays', () => { + const persistedState = new PersistedState({ + hello: { nouns: ['world', 'humans', 'everyone'] }, + }); + + expect(persistedState.get()).toEqual({ hello: { nouns: ['world', 'humans', 'everyone'] } }); + expect(persistedState.get('hello')).toEqual({ nouns: ['world', 'humans', 'everyone'] }); + expect(persistedState.get('hello.nouns')).toEqual(['world', 'humans', 'everyone']); + }); + }); + + describe('set state', () => { + describe('path format support', () => { + test('should create deep objects from dot notation', () => { + const persistedState = new PersistedState(); + persistedState.set('one.two.three', 4); + expect(persistedState.get()).toEqual({ one: { two: { three: 4 } } }); + }); + + test('should create deep objects from array notation', () => { + const persistedState = new PersistedState(); + persistedState.set('one[two][three]', 4); + expect(persistedState.get()).toEqual({ one: { two: { three: 4 } } }); + }); + + test('should create deep objects from arrays', () => { + const persistedState = new PersistedState(); + persistedState.set(['one', 'two', 'three'], 4); + expect(persistedState.get()).toEqual({ one: { two: { three: 4 } } }); + }); + + test('should create deep objects with an existing path', () => { + const persistedState = new PersistedState({}, 'deep.path'); + persistedState.set('green[red].blue', 4); + expect(persistedState.get()).toEqual({ green: { red: { blue: 4 } } }); + }); + }); + + describe('simple replace operations', () => { + let persistedState; + + test('should replace value with string', () => { + persistedState = new PersistedState({ hello: 'world' }); + expect(persistedState.get()).toEqual({ hello: 'world' }); + + persistedState.set('hello', 'fare thee well'); + expect(persistedState.get()).toEqual({ hello: 'fare thee well' }); + }); + + test('should replace value with array', () => { + persistedState = new PersistedState({ hello: ['world', 'everyone'] }); + expect(persistedState.get()).toEqual({ hello: ['world', 'everyone'] }); + + persistedState.set('hello', ['people']); + expect(persistedState.get()).toEqual({ hello: ['people'] }); + }); + + test('should replace value with object', () => { + persistedState = new PersistedState({ hello: 'world' }); + expect(persistedState.get()).toEqual({ hello: 'world' }); + + persistedState.set('hello', { message: 'fare thee well' }); + expect(persistedState.get()).toEqual({ hello: { message: 'fare thee well' } }); + }); + + test('should replace value with object, removing old properties', () => { + persistedState = new PersistedState({ hello: { message: 'world' } }); + expect(persistedState.get()).toEqual({ hello: { message: 'world' } }); + + persistedState.set('hello', { length: 5 }); + expect(persistedState.get()).toEqual({ hello: { length: 5 } }); + }); + }); + + describe('deep replace operations', () => { + let persistedState; + + test('should append to the object', () => { + persistedState = new PersistedState({ hello: { message: 'world' } }); + expect(persistedState.get()).toEqual({ hello: { message: 'world' } }); + + persistedState.set('hello.length', 5); + expect(persistedState.get()).toEqual({ hello: { message: 'world', length: 5 } }); + }); + + test('should change the value in the array', () => { + persistedState = new PersistedState({ hello: { nouns: ['world', 'humans', 'everyone'] } }); + persistedState.set('hello.nouns[1]', 'aliens'); + + expect(persistedState.get()).toEqual({ hello: { nouns: ['world', 'aliens', 'everyone'] } }); + expect(persistedState.get('hello')).toEqual({ nouns: ['world', 'aliens', 'everyone'] }); + expect(persistedState.get('hello.nouns')).toEqual(['world', 'aliens', 'everyone']); + }); + }); + }); + + describe('internal state tracking', () => { + test('should be an empty object', () => { + const persistedState = new PersistedState(); + expect(persistedState._defaultState).toEqual({}); + }); + + test('should store the default state value', () => { + const val = { one: 1, two: 2 }; + const persistedState = new PersistedState(val); + expect(persistedState._defaultState).toEqual(val); + }); + + test('should keep track of changes', () => { + const val = { one: 1, two: 2 }; + const persistedState = new PersistedState(val); + + persistedState.set('two', 22); + expect(persistedState._defaultState).toEqual(val); + expect(persistedState._changedState).toEqual({ two: 22 }); + }); + }); + + describe('events', () => { + let persistedState: PersistedState; + let emitSpy: jest.SpyInstance; + + const getByType = (type: string): any[] => { + return emitSpy.mock.calls.filter(([callType]) => callType === type); + }; + + const watchEmitter = (state: any) => { + return jest.spyOn(state, 'emit'); + }; + + beforeEach(() => { + persistedState = new PersistedState({ checker: { events: 'event tests' } }); + emitSpy = watchEmitter(persistedState); + }); + + afterEach(() => { + emitSpy.mockRestore(); + }); + + test('should emit set when setting values', () => { + expect(getByType('set')).toHaveLength(0); + persistedState.set('checker.time', 'now'); + expect(getByType('set')).toHaveLength(1); + }); + + test('should not emit when setting value silently', () => { + expect(getByType('set')).toHaveLength(0); + persistedState.setSilent('checker.time', 'now'); + expect(getByType('set')).toHaveLength(0); + }); + + test('should emit change when changing values', () => { + expect(getByType('change')).toHaveLength(0); + persistedState.set('checker.time', 'now'); + expect(getByType('change')).toHaveLength(1); + }); + + test('should not emit when changing values silently', () => { + expect(getByType('change')).toHaveLength(0); + persistedState.setSilent('checker.time', 'now'); + expect(getByType('change')).toHaveLength(0); + }); + + test('should not emit change when values are identical', () => { + expect(getByType('change')).toHaveLength(0); + // check both forms of setting the same value + persistedState.set('checker', { events: 'event tests' }); + expect(getByType('change')).toHaveLength(0); + persistedState.set('checker.events', 'event tests'); + expect(getByType('change')).toHaveLength(0); + }); + + test('should emit change when values change', () => { + expect(getByType('change')).toHaveLength(0); + persistedState.set('checker.events', 'i changed'); + expect(getByType('change')).toHaveLength(1); + }); + }); +}); diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js index e253d49c04131..279aff62f55af 100644 --- a/src/legacy/ui/public/state_management/app_state.js +++ b/src/legacy/ui/public/state_management/app_state.js @@ -29,14 +29,14 @@ import { uiModules } from '../modules'; import { StateProvider } from './state'; -import '../persisted_state'; +import { PersistedState } from '../persisted_state'; import { createLegacyClass } from '../utils/legacy_class'; const urlParam = '_a'; -export function AppStateProvider(Private, $location, $injector) { +export function AppStateProvider(Private, $location) { const State = Private(StateProvider); - const PersistedState = $injector.get('PersistedState'); + let persistedStates; let eventUnsubscribers; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index 09ac0f95a1dd9..c14b64a32fb5c 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -18,8 +18,6 @@ import { GlobalStateProvider, StateManagementConfigProvider, AppStateProvider, - EventsProvider, - PersistedState, KbnUrlProvider, RedirectWhenMissingProvider, npStart, @@ -43,7 +41,6 @@ export const localAppModule = (core: AppMountContext['core']) => { createLocalConfigModule(core); createLocalKbnUrlModule(); createLocalStateModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(npStart.plugins.navigation); createHrefModule(core); @@ -52,7 +49,6 @@ export const localAppModule = (core: AppMountContext['core']) => { 'monitoring/Config', 'monitoring/I18n', 'monitoring/Private', - 'monitoring/PersistedState', 'monitoring/TopNav', 'monitoring/State', 'monitoring/Storage', @@ -71,7 +67,6 @@ function createLocalStateModule() { 'monitoring/Config', 'monitoring/KbnUrl', 'monitoring/Promise', - 'monitoring/PersistedState', ]) .factory('AppState', function(Private: IPrivate) { return Private(AppStateProvider); @@ -81,19 +76,6 @@ function createLocalStateModule() { }); } -function createLocalPersistedStateModule() { - angular - .module('monitoring/PersistedState', ['monitoring/Private', 'monitoring/Promise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: string) { - super(value, path, Events); - } - }; - }); -} - function createLocalKbnUrlModule() { angular .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute'])