[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
This commit is contained in:
Daniil Suleiman 2020-02-20 16:15:43 +03:00 committed by GitHub
parent a1bf7d44d4
commit ef8c36fe75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 400 additions and 168 deletions

View file

@ -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: [
{

View file

@ -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';

View file

@ -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<AppState>(AppStateProvider).getAppState;
})
.service('globalState', function(Private: IPrivate) {
return Private(GlobalStateProvider);
});

View file

@ -2,7 +2,7 @@
<!-- Linked search. -->
<div
ng-show="isVisible"
ng-if="vis.type.requiresSearch && state.linked"
ng-if="vis.type.requiresSearch && linked"
class="fullWidth visEditor__linkedMessage"
>
<div class="kuiVerticalRhythmSmall">
@ -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"
/>
<h1
class="euiScreenReaderOnly"
@ -117,6 +119,7 @@
filters="filters"
query="query"
class="visEditor__content"
app-state="appState"
/>
</visualize-app>

View file

@ -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);
const useHash = config.get('state:storeInSessionStorage');
const { stateContainer, stopStateSync } = useVisualizeAppState({
useHash,
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);
const filterStateManager = new FilterStateManager(
globalState,
() => {
// Temporary AppState replacement
return {
set filters(_filters) {
stateContainer.transitions.set('filters', _filters);
},
get filters() {
return stateContainer.getState().filters;
},
};
},
filterManager
);
// 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',
})
);
// 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',
});
}
return appState;
})();
}
$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,13 +343,27 @@ function VisualizeAppController(
});
}
$scope.searchSource = searchSource;
$scope.state = $state;
$scope.refreshInterval = timefilter.getRefreshInterval();
const initialState = stateContainer.getState();
// Create a PersistedState instance.
$scope.uiState = $state.makeStateful('uiState');
$scope.appStatus = $appStatus;
$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.refreshInterval = timefilter.getRefreshInterval();
const addToDashMode =
$route.current.params[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;
});
$scope.$watch('state.query', (newQuery, oldQuery) => {
if (!_.isEqual(newQuery, oldQuery)) {
const query = migrateLegacyQuery(newQuery);
if (!_.isEqual(query, newQuery)) {
$state.query = query;
}
$scope.fetch();
const unsubscribeStateUpdates = stateContainer.subscribe(state => {
const newQuery = migrateLegacyQuery(state.query);
if (!_.isEqual(state.query, newQuery)) {
stateContainer.transitions.set('query', newQuery);
}
});
persistOnChange(state);
$state.replace();
// 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');
}
$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);

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { migrateAppState } from './migrate_app_state';
export { useVisualizeAppState } from './visualize_app_state';
export { makeStateful } from './make_stateful';

View file

@ -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<VisualizeAppState, VisualizeAppStateTransitions>
) {
// 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 };
}

View file

@ -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<VisualizeAppState>(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;
});
if (splitCount <= 1) {
return appState; // do nothing; we only want to touch tables with multiple split aggs
}
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;
});
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;
}

View file

@ -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<VisualizeAppState>(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<VisualizeAppState, VisualizeAppStateTransitions>(
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 };
}

View file

@ -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]);

View file

@ -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,
});
};

View file

@ -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<Vis['getCurrentState']>;
export interface VisualizeAppState {
filters: Filter[];
uiState: PersistedState;
vis: PureVisState;
query: Query;
savedQuery?: string;
linked: boolean;
}
export interface VisualizeAppStateTransitions {
set: (
state: VisualizeAppState
) => <T extends keyof VisualizeAppState>(
prop: T,
value: VisualizeAppState[T]
) => VisualizeAppState;
setVis: (state: VisualizeAppState) => (vis: Partial<PureVisState>) => 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;

View file

@ -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,

View file

@ -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);

View file

@ -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<typeof npStart.plugins.expressions.Expressi
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
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<VisualizeInput, VisualizeOut
private handleVisUpdate = async () => {
if (this.appState) {
this.appState.vis = this.savedVisualization.vis.getState();
this.appState.save();
}

View file

@ -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<Vis['getCurrentState']>;
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<Vis['getCurrentState']>;
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();
});

View file

@ -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<Vis['getCurrentState']>,
schemas: Schemas,
uiState: any,
meta?: { savedObjectId?: string }

View file

@ -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

View file

@ -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

View file

@ -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',