From ef8c36fe754dec49fafb40a385470e462995deb4 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 20 Feb 2020 16:15:43 +0300 Subject: [PATCH] [Visualize] Remove legacy appState in visualize (#57330) (#58105) * Remove legacy appState in visualize * Read query and linked prop from scope * Fix persisted state instance * Fix functional tests * Bound url updates with an editor * Some improvements * Fix comments --- .../components/editor/controls_tab.test.tsx | 5 +- .../kibana/public/visualize/legacy_imports.ts | 3 - .../public/visualize/np_ready/application.ts | 8 - .../visualize/np_ready/editor/editor.html | 11 +- .../visualize/np_ready/editor/editor.js | 218 +++++++++--------- .../editor/lib/{index.js => index.ts} | 3 +- .../np_ready/editor/lib/make_stateful.ts | 58 +++++ ...rate_app_state.js => migrate_app_state.ts} | 47 ++-- .../editor/lib/visualize_app_state.ts | 112 +++++++++ .../np_ready/editor/visualization.js | 5 +- .../np_ready/editor/visualization_editor.js | 5 +- .../public/visualize/np_ready/types.d.ts | 31 ++- .../public/components/sidebar/sidebar.tsx | 17 +- .../public/components/vis_editor.js | 1 - .../public/embeddable/visualize_embeddable.ts | 8 +- .../public/legacy/build_pipeline.test.ts | 10 +- .../np_ready/public/legacy/build_pipeline.ts | 4 +- .../public/np_ready/public/vis.ts | 8 + .../public/np_ready/public/vis_impl.d.ts | 10 +- .../apps/visualize/_heatmap_chart.js | 4 +- 20 files changed, 400 insertions(+), 168 deletions(-) rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{index.js => index.ts} (87%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{migrate_app_state.js => migrate_app_state.ts} (61%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 3419d773bd09e6..d7a62e07b26f31 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -23,6 +23,7 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, @@ -32,7 +33,7 @@ let props: ControlsTabUiProps; beforeEach(() => { props = { deps: getDepsMock(), - vis: { + vis: ({ API: { indexPatterns: indexPatternsMock, }, @@ -46,7 +47,7 @@ beforeEach(() => { requiresSearch: false, hidden: false, }, - }, + } as unknown) as Vis, stateParams: { controls: [ { diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 92433799ba4204..8b1bb0fda8c841 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,14 +24,11 @@ * directly where they are needed. */ -// @ts-ignore -export { AppState, AppStateProvider } from 'ui/state_management/app_state'; export { State } from 'ui/state_management/state'; // @ts-ignore export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { PersistedState } from 'ui/persisted_state'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; 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 bd7b478f827a65..6a8d9ce106f9dc 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 @@ -22,8 +22,6 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { - AppStateProvider, - AppState, configureAppAngularModule, createTopNavDirective, createTopNavHelper, @@ -116,12 +114,6 @@ function createLocalStateModule() { 'app/visualize/Promise', 'app/visualize/PersistedState', ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: IPrivate) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: IPrivate) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 6190b92c9be3e1..4979d9dc89a0ce 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -2,7 +2,7 @@
@@ -42,9 +42,9 @@ show-filter-bar="showFilterBar() && isVisible" show-date-picker="showQueryBarTimePicker()" show-auto-refresh-only="!showQueryBarTimePicker()" - query="state.query" + query="query" saved-query="savedQuery" - screen-title="state.vis.title" + screen-title="vis.title" on-query-submit="updateQueryAndFetch" index-patterns="[indexPattern]" filters="filters" @@ -97,7 +97,9 @@ ui-state="uiState" time-range="timeRange" filters="filters" - query="query"/> + query="query" + app-state="appState" + />

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 409d4b41fbe696..415949f88e9d17 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { migrateAppState } from './lib'; +import { makeStateful, useVisualizeAppState } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -45,7 +45,6 @@ import { absoluteToParsedUrl, KibanaParsedUrl, migrateLegacyQuery, - stateMonitorFactory, DashboardConstants, } from '../../legacy_imports'; @@ -68,15 +67,14 @@ function VisualizeAppController( $scope, $element, $route, - AppState, $window, $injector, $timeout, kbnUrl, redirectWhenMissing, Promise, - getAppState, - globalState + globalState, + config ) { const { indexPatterns, @@ -99,7 +97,6 @@ function VisualizeAppController( setActiveUrl, } = getServices(); - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -113,9 +110,9 @@ function VisualizeAppController( $scope.vis = vis; - const $appStatus = (this.appStatus = { + const $appStatus = { dirty: !savedVis.id, - }); + }; vis.on('dirtyStateChange', ({ isDirty }) => { vis.dirty = isDirty; @@ -265,53 +262,61 @@ function VisualizeAppController( }, ]; - let stateMonitor; - if (savedVis.id) { chrome.docTitle.change(savedVis.title); } + const defaultQuery = { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + }; + // Extract visualization state with filtered aggs. You can see these filtered aggs in the URL. // Consists of things like aggs, params, listeners, title, type, etc. const savedVisState = vis.getState(); const stateDefaults = { uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, - linked: !!savedVis.savedSearchId, - query: searchSource.getOwnField('query') || { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, + query: searchSource.getOwnField('query') || defaultQuery, filters: searchSource.getOwnField('filter') || [], vis: savedVisState, + linked: !!savedVis.savedSearchId, }; - // Instance of app_state.js. - const $state = (function initState() { - // This is used to sync visualization state with the url when `appState.save()` is called. - const appState = new AppState(stateDefaults); - - // Initializing appState does two things - first it translates the defaults into AppState, - // second it updates appState based on the url (the url trumps the defaults). This means if - // we update the state format at all and want to handle BWC, we must not only migrate the - // data stored with saved vis, but also any old state in the url. - migrateAppState(appState); - - // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the - // defaults applied. If the url was from a previous session which included modifications to the - // appState then they won't be equal. - if (!angular.equals(appState.vis, savedVisState)) { - Promise.try(function() { - vis.setState(appState.vis); - }).catch( - redirectWhenMissing({ - 'index-pattern-field': '/visualize', - }) - ); - } + const useHash = config.get('state:storeInSessionStorage'); + const { stateContainer, stopStateSync } = useVisualizeAppState({ + useHash, + stateDefaults, + }); + + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters) { + stateContainer.transitions.set('filters', _filters); + }, + get filters() { + return stateContainer.getState().filters; + }, + }; + }, + filterManager + ); - return appState; - })(); + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the + // defaults applied. If the url was from a previous session which included modifications to the + // appState then they won't be equal. + if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { + try { + vis.setState(stateContainer.getState().vis); + } catch { + redirectWhenMissing({ + 'index-pattern-field': '/visualize', + }); + } + } $scope.filters = filterManager.getFilters(); @@ -330,8 +335,6 @@ function VisualizeAppController( ); function init() { - // export some objects - $scope.savedVis = savedVis; if (vis.indexPattern) { $scope.indexPattern = vis.indexPattern; } else { @@ -340,14 +343,28 @@ function VisualizeAppController( }); } + const initialState = stateContainer.getState(); + + $scope.appState = { + // mock implementation of the legacy appState.save() + // this could be even replaced by passing only "updateAppState" callback + save() { + stateContainer.transitions.updateVisState(vis.getState()); + }, + }; + + // Create a PersistedState instance for uiState. + const { persistedState, unsubscribePersisted, persistOnChange } = makeStateful( + 'uiState', + stateContainer + ); + $scope.uiState = persistedState; + $scope.savedVis = savedVis; + $scope.query = initialState.query; + $scope.linked = initialState.linked; $scope.searchSource = searchSource; - $scope.state = $state; $scope.refreshInterval = timefilter.getRefreshInterval(); - // Create a PersistedState instance. - $scope.uiState = $state.makeStateful('uiState'); - $scope.appStatus = $appStatus; - const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); @@ -372,22 +389,23 @@ function VisualizeAppController( $scope.timeRange = timefilter.getTime(); $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode'); - stateMonitor = stateMonitorFactory.create($state, stateDefaults); - stateMonitor.ignoreProps(['vis.listeners']).onChange(status => { - $appStatus.dirty = status.dirty || !savedVis.id; - }); + const unsubscribeStateUpdates = stateContainer.subscribe(state => { + const newQuery = migrateLegacyQuery(state.query); + if (!_.isEqual(state.query, newQuery)) { + stateContainer.transitions.set('query', newQuery); + } + persistOnChange(state); - $scope.$watch('state.query', (newQuery, oldQuery) => { - if (!_.isEqual(newQuery, oldQuery)) { - const query = migrateLegacyQuery(newQuery); - if (!_.isEqual(query, newQuery)) { - $state.query = query; - } - $scope.fetch(); + // if the browser history was changed manually we need to reflect changes in the editor + if (!_.isEqual(vis.getState(), state.vis)) { + vis.setState(state.vis); + vis.forceReload(); + vis.emit('updateEditor'); } - }); - $state.replace(); + $appStatus.dirty = true; + $scope.fetch(); + }); const updateTimeRange = () => { $scope.timeRange = timefilter.getTime(); @@ -419,10 +437,11 @@ function VisualizeAppController( // update the searchSource when query updates $scope.fetch = function() { - $state.save(); - $scope.query = $state.query; - savedVis.searchSource.setField('query', $state.query); - savedVis.searchSource.setField('filter', $state.filters); + const { query, filters, linked } = stateContainer.getState(); + $scope.query = query; + $scope.linked = linked; + savedVis.searchSource.setField('query', query); + savedVis.searchSource.setField('filter', filters); $scope.$broadcast('render'); }; @@ -446,10 +465,13 @@ function VisualizeAppController( $scope._handler.destroy(); } savedVis.destroy(); - stateMonitor.destroy(); filterStateManager.destroy(); subscriptions.unsubscribe(); $scope.vis.off('apply', _applyVis); + + unsubscribePersisted(); + unsubscribeStateUpdates(); + stopStateSync(); }); $timeout(() => { @@ -459,10 +481,10 @@ function VisualizeAppController( $scope.updateQueryAndFetch = function({ query, dateRange }) { const isUpdate = - (query && !_.isEqual(query, $state.query)) || + (query && !_.isEqual(query, stateContainer.getState().query)) || (dateRange && !_.isEqual(dateRange, $scope.timeRange)); - $state.query = query; + stateContainer.transitions.set('query', query); timefilter.setTime(dateRange); // If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes @@ -488,20 +510,13 @@ function VisualizeAppController( $scope.onClearSavedQuery = () => { delete $scope.savedQuery; - delete $state.savedQuery; - $state.query = { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }; + stateContainer.transitions.removeSavedQuery(defaultQuery); filterManager.setFilters(filterManager.getGlobalFilters()); - $state.save(); $scope.fetch(); }; const updateStateFromSavedQuery = savedQuery => { - $state.query = savedQuery.attributes.query; - $state.save(); + stateContainer.transitions.set('query', savedQuery.attributes.query); const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -520,44 +535,38 @@ function VisualizeAppController( $scope.fetch(); }; + // update the query if savedQuery is stored + if (stateContainer.getState().savedQuery) { + savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => { + $scope.$evalAsync(() => { + $scope.savedQuery = savedQuery; + }); + }); + } + $scope.$watch('savedQuery', newSavedQuery => { if (!newSavedQuery) return; - $state.savedQuery = newSavedQuery.id; - $state.save(); + stateContainer.transitions.set('savedQuery', newSavedQuery.id); updateStateFromSavedQuery(newSavedQuery); }); - $scope.$watch('state.savedQuery', newSavedQueryId => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - }); - /** * Called when the user clicks "Save" button. */ function doSave(saveOptions) { // vis.title was not bound and it's needed to reflect title into visState - $state.vis.title = savedVis.title; - $state.vis.type = savedVis.type || $state.vis.type; - savedVis.visState = $state.vis; + stateContainer.transitions.setVis({ + title: savedVis.title, + type: savedVis.type || stateContainer.getState().vis.type, + }); + savedVis.visState = stateContainer.getState().vis; savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); + $appStatus.dirty = false; return savedVis.save(saveOptions).then( function(id) { $scope.$evalAsync(() => { - stateMonitor.setInitialState($state.toJSON()); - if (id) { toastNotifications.addSuccess({ title: i18n.translate( @@ -601,8 +610,6 @@ function VisualizeAppController( chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); savedVis.vis.title = savedVis.title; savedVis.vis.description = savedVis.description; - // it's needed to save the state to update url string - $state.save(); } else { kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }); } @@ -632,9 +639,8 @@ function VisualizeAppController( } $scope.unlink = function() { - if (!$state.linked) return; + if (!$scope.linked) return; - $state.linked = false; const searchSourceParent = searchSource.getParent(); const searchSourceGrandparent = searchSourceParent.getParent(); @@ -645,8 +651,10 @@ function VisualizeAppController( _.union(searchSource.getOwnField('filter'), searchSourceParent.getOwnField('filter')) ); - $state.query = searchSourceParent.getField('query'); - $state.filters = searchSourceParent.getField('filter'); + stateContainer.transitions.unlinkSavedSearch( + searchSourceParent.getField('query'), + searchSourceParent.getField('filter') + ); searchSource.setField('index', searchSourceParent.getField('index')); searchSource.setParent(searchSourceGrandparent); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts similarity index 87% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index 42284c9a03dcd0..fa5b91b00edaf1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { migrateAppState } from './migrate_app_state'; +export { useVisualizeAppState } from './visualize_app_state'; +export { makeStateful } from './make_stateful'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts new file mode 100644 index 00000000000000..137d4de1fe9a82 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts @@ -0,0 +1,58 @@ +/* + * 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 { PersistedState } from '../../../legacy_imports'; +import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +/** + * @returns Create a PersistedState instance, initialize state changes subscriber/unsubscriber + */ +export function makeStateful( + prop: keyof VisualizeAppState, + stateContainer: ReduxLikeStateContainer +) { + // set up the persistedState state + const persistedState = new PersistedState(); + + // update the appState when the stateful instance changes + const updateOnChange = function() { + stateContainer.transitions.set(prop, persistedState.getChanges()); + }; + + const handlerOnChange = (method: 'on' | 'off') => + persistedState[method]('change', updateOnChange); + + handlerOnChange('on'); + const unsubscribePersisted = () => handlerOnChange('off'); + + // update the stateful object when the app state changes + const persistOnChange = function(state: VisualizeAppState) { + if (state[prop]) { + persistedState.set(state[prop]); + } + }; + + const appState = stateContainer.getState(); + + // if the thing we're making stateful has an appState value, write to persisted state + if (appState[prop]) persistedState.setSilent(appState[prop]); + + return { persistedState, unsubscribePersisted, persistOnChange }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts similarity index 61% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts index 049ce048239db8..7e09aece52e096 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts @@ -18,6 +18,7 @@ */ import { get, omit } from 'lodash'; +import { VisualizeAppState } from '../../types'; /** * Creates a new instance of AppState based on the table vis state. @@ -25,37 +26,41 @@ import { get, omit } from 'lodash'; * Dashboards have a similar implementation; see * core_plugins/kibana/public/dashboard/lib/migrate_app_state * - * @param appState {AppState} AppState class to instantiate + * @param appState {VisualizeAppState} */ -export function migrateAppState(appState) { +export function migrateAppState(appState: VisualizeAppState) { // For BWC in pre 7.0 versions where table visualizations could have multiple aggs // with `schema === 'split'`. This ensures that bookmarked URLs with deprecated params // are rewritten to the correct state. See core_plugins/table_vis/migrations. if (appState.vis.type !== 'table') { - return; + return appState; } - const visAggs = get(appState, 'vis.aggs', []); - let splitCount = 0; - const migratedAggs = visAggs.map(agg => { - if (agg.schema !== 'split') { + const visAggs: any = get(appState, 'vis.aggs'); + + if (visAggs) { + let splitCount = 0; + const migratedAggs = visAggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); return agg; - } + }); - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged + if (splitCount <= 1) { + return appState; // do nothing; we only want to touch tables with multiple split aggs } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return; // do nothing; we only want to touch tables with multiple split aggs + + appState.vis.aggs = migratedAggs; } - appState.vis.aggs = migratedAggs; - appState.save(); + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts new file mode 100644 index 00000000000000..d8de81193d857f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -0,0 +1,112 @@ +/* + * 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 { createHashHistory } from 'history'; +import { isFunction, omit } from 'lodash'; + +import { migrateAppState } from './migrate_app_state'; +import { + createKbnUrlStateStorage, + createStateContainer, + syncState, +} from '../../../../../../../../plugins/kibana_utils/public'; +import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + useHash: boolean; + stateDefaults: VisualizeAppState; +} + +function toObject(state: PureVisState): PureVisState { + return omit(state, (value, key: string) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || isFunction(value); + }); +} + +export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { + const history = createHashHistory(); + const kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash, + history, + }); + const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); + const initialState = migrateAppState({ + ...stateDefaults, + ...urlState, + }); + + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setVis: state => vis => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + removeSavedQuery: state => defaultQuery => { + const { savedQuery, ...rest } = state; + + return { + ...rest, + query: defaultQuery, + }; + }, + unlinkSavedSearch: state => (query, filters) => ({ + ...state, + query, + filters, + linked: false, + }), + updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), + } + ); + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + set: state => { + if (state) { + // syncState utils requires to handle incoming "null" value + stateContainer.set(state); + } + }, + }, + stateStorage: kbnUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + + return { stateContainer, stopStateSync }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 502bd6e56fb1f9..6acdb0abdd0b50 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -18,7 +18,7 @@ */ export function initVisualizationDirective(app, deps) { - app.directive('visualizationEmbedded', function($timeout, getAppState) { + app.directive('visualizationEmbedded', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisualizationDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { $scope.renderFunction = async () => { @@ -37,7 +38,7 @@ export function initVisualizationDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters || [], query: $scope.query, - appState: getAppState(), + appState: $scope.appState, uiState: $scope.uiState, }); $scope._handler.render(element[0]); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index 8032152f88173a..c40a10115ae4eb 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -18,7 +18,7 @@ */ export function initVisEditorDirective(app, deps) { - app.directive('visualizationEditor', function($timeout, getAppState) { + app.directive('visualizationEditor', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisEditorDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { const Editor = $scope.savedObj.vis.type.editor; @@ -41,7 +42,7 @@ export function initVisEditorDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters, query: $scope.query, - appState: getAppState(), + appState: $scope.appState, }); }; 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 524bc4b3196b7c..139c247aa29cc4 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,10 +20,37 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; -import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; +import { VisState, Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { VisSavedObject, PersistedState } from '../legacy_imports'; + +export type PureVisState = ReturnType; + +export interface VisualizeAppState { + filters: Filter[]; + uiState: PersistedState; + vis: PureVisState; + query: Query; + savedQuery?: string; + linked: boolean; +} + +export interface VisualizeAppStateTransitions { + set: ( + state: VisualizeAppState + ) => ( + prop: T, + value: VisualizeAppState[T] + ) => VisualizeAppState; + setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; + removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; + unlinkSavedSearch: ( + state: VisualizeAppState + ) => (query: Query, filters: Filter[]) => VisualizeAppState; + updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; +} export interface EditorRenderProps { - appState: AppState; + appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; embeddable: IEmbeddableStart; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index e33e83fd19fec6..8615bcdd1bfbd5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -26,7 +26,7 @@ import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { PersistedState, AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; -import { setStateParamValue, useEditorReducer, useEditorFormState } from './state'; +import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; interface DefaultEditorSideBarProps { @@ -104,15 +104,26 @@ function DefaultEditorSideBar({ ); useEffect(() => { - vis.on('dirtyStateChange', ({ isDirty: dirty }: { isDirty: boolean }) => { + const changeHandler = ({ isDirty: dirty }: { isDirty: boolean }) => { setDirty(dirty); if (!dirty) { resetValidity(); } - }); + }; + vis.on('dirtyStateChange', changeHandler); + + return () => vis.off('dirtyStateChange', changeHandler); }, [resetValidity, vis]); + // subscribe on external vis changes using browser history, for example press back button + useEffect(() => { + const resetHandler = () => dispatch(discardChanges(vis)); + vis.on('updateEditor', resetHandler); + + return () => vis.off('updateEditor', resetHandler); + }, [dispatch, vis]); + const dataTabProps = { dispatch, formIsTouched: formState.touched, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index b2dd1813e6d208..0263f5b2c851c1 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -82,7 +82,6 @@ export class VisEditor extends Component { // This check should be redundant, since this method should only be called when we're in editor // mode where there's also an appState passed into us. if (this.props.appState) { - this.props.appState.vis = this.props.vis.getState(); this.props.appState.save(); } }, VIS_STATE_DEBOUNCE_DELAY); diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 9e388832283faa..e7ca5ea8037016 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -23,7 +23,6 @@ import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; @@ -68,7 +67,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editUrl: string; editable: boolean; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -79,7 +78,7 @@ export interface VisualizeInput extends EmbeddableInput { vis?: { colors?: { [key: string]: string }; }; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -95,7 +94,7 @@ type ExpressionLoader = InstanceType { private handler?: ExpressionLoader; private savedVisualization: VisSavedObject; - private appState: AppState | undefined; + private appState: { save(): void } | undefined; private uiState: PersistedState; private timeRange?: TimeRange; private query?: Query; @@ -389,7 +388,6 @@ export class VisualizeEmbeddable extends Embeddable { if (this.appState) { - this.appState.vis = this.savedVisualization.vis.getState(); this.appState.save(); } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index d1017de35474a9..f73dc3e19d0ef3 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -26,7 +26,7 @@ import { SchemaConfig, Schemas, } from './build_pipeline'; -import { Vis, VisState } from '..'; +import { Vis } from '..'; import { IAggConfig } from '../../../legacy_imports'; import { searchSourceMock } from '../../../legacy_mocks'; @@ -83,7 +83,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }); describe('buildPipelineVisFunction', () => { - let visStateDef: VisState; + let visStateDef: ReturnType; let schemaConfig: SchemaConfig; let schemasDef: Schemas; let uiState: any; @@ -94,7 +94,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { // @ts-ignore type: 'type', params: {}, - }; + } as ReturnType; schemaConfig = { accessor: 0, @@ -349,7 +349,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { describe('buildPipeline', () => { it('calls toExpression on vis_type if it exists', async () => { - const vis: Vis = { + const vis = ({ getCurrentState: () => {}, getUiState: () => null, isHierarchical: () => false, @@ -360,7 +360,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { type: { toExpression: () => 'testing custom expressions', }, - }; + } as unknown) as Vis; const expression = await buildPipeline(vis, { searchSource: searchSourceMock }); expect(expression).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 04a296a888e87f..025eef834ca86b 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -28,7 +28,7 @@ import { isDateHistogramBucketAggConfig, createFormat, } from '../../../legacy_imports'; -import { Vis, VisParams, VisState } from '..'; +import { Vis, VisParams } from '..'; interface SchemaConfigParams { precision?: number; @@ -59,7 +59,7 @@ export interface Schemas { } type buildVisFunction = ( - visState: VisState, + visState: ReturnType, schemas: Schemas, uiState: any, meta?: { savedObjectId?: string } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index 59a8013523ef60..19375e25a9fb72 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -23,6 +23,14 @@ import { Status } from './legacy/update_status'; export interface Vis { type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts index 45d65efb5dcdff..f9b7db5c02d937 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Vis, VisState } from './vis'; +import { Vis, VisState, VisParams } from './vis'; import { VisType } from './types'; import { IIndexPattern } from '../../../../../../plugins/data/common'; @@ -35,6 +35,14 @@ export declare class VisImpl implements Vis { constructor(indexPattern: IIndexPattern, visState?: InitVisStateType); type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 2cea861d0f64df..cbf7ab8df6831e 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -96,6 +96,8 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickOptionsTab(); await PageObjects.visEditor.changeHeatmapColorNumbers(6); await PageObjects.visEditor.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 267', @@ -121,9 +123,9 @@ export default function({ getService, getPageObjects }) { log.debug('customize 2 last ranges'); await PageObjects.visEditor.setCustomRangeByIndex(6, '650', '720'); await PageObjects.visEditor.setCustomRangeByIndex(7, '800', '905'); + await PageObjects.visEditor.clickGo(); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 100',