mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* 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:
parent
a1bf7d44d4
commit
ef8c36fe75
20 changed files with 400 additions and 168 deletions
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { migrateAppState } from './migrate_app_state';
|
||||
export { useVisualizeAppState } from './visualize_app_state';
|
||||
export { makeStateful } from './make_stateful';
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue