From e16961d696c6deee2303c70e78f5346f2895aa9a Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 27 Jan 2020 18:30:47 +0100 Subject: [PATCH 01/29] wip --- .../state_containers_examples/common/index.ts | 21 ++ .../state_containers_examples/kibana.json | 4 +- .../public/plugin.ts | 23 +- .../public/{ => todo}/app.tsx | 0 .../public/{ => todo}/todo.tsx | 6 +- .../public/with_data_services/application.tsx | 49 ++++ .../with_data_services/components/app.tsx | 244 ++++++++++++++++++ .../public/with_data_services/types.ts | 31 +++ .../state_containers_examples/server/index.ts | 28 ++ .../server/plugin.ts | 56 ++++ .../server/routes/index.ts | 36 +++ .../state_containers_examples/server/types.ts | 23 ++ .../data/public/query/state_sync/index.ts | 2 +- .../query/state_sync/sync_app_filters.ts | 24 +- .../public/query/state_sync/sync_query.ts | 136 +++------- .../ui/search_bar/create_search_bar.tsx | 10 +- .../data/public/ui/search_bar/search_bar.tsx | 5 +- 17 files changed, 571 insertions(+), 127 deletions(-) create mode 100644 examples/state_containers_examples/common/index.ts rename examples/state_containers_examples/public/{ => todo}/app.tsx (100%) rename examples/state_containers_examples/public/{ => todo}/todo.tsx (98%) create mode 100644 examples/state_containers_examples/public/with_data_services/application.tsx create mode 100644 examples/state_containers_examples/public/with_data_services/components/app.tsx create mode 100644 examples/state_containers_examples/public/with_data_services/types.ts create mode 100644 examples/state_containers_examples/server/index.ts create mode 100644 examples/state_containers_examples/server/plugin.ts create mode 100644 examples/state_containers_examples/server/routes/index.ts create mode 100644 examples/state_containers_examples/server/types.ts diff --git a/examples/state_containers_examples/common/index.ts b/examples/state_containers_examples/common/index.ts new file mode 100644 index 0000000000000..25dc2eacf9c75 --- /dev/null +++ b/examples/state_containers_examples/common/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export const PLUGIN_ID = 'stateContainersExampleWithDataServices'; +export const PLUGIN_NAME = 'State containers example - with data services'; diff --git a/examples/state_containers_examples/kibana.json b/examples/state_containers_examples/kibana.json index 9114a414a4da3..437e9a4fac63c 100644 --- a/examples/state_containers_examples/kibana.json +++ b/examples/state_containers_examples/kibana.json @@ -3,8 +3,8 @@ "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["state_containers_examples"], - "server": false, + "server": true, "ui": true, - "requiredPlugins": [], + "requiredPlugins": ["navigation", "data"], "optionalPlugins": [] } diff --git a/examples/state_containers_examples/public/plugin.ts b/examples/state_containers_examples/public/plugin.ts index beb7b93dbc5b6..38ebf315789c0 100644 --- a/examples/state_containers_examples/public/plugin.ts +++ b/examples/state_containers_examples/public/plugin.ts @@ -18,14 +18,16 @@ */ import { AppMountParameters, CoreSetup, Plugin } from 'kibana/public'; +import { AppPluginDependencies } from './with_data_services/types'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; export class StateContainersExamplesPlugin implements Plugin { public setup(core: CoreSetup) { core.application.register({ - id: 'state-containers-example-browser-history', + id: 'stateContainersExampleBrowserHistory', title: 'State containers example - browser history routing', async mount(params: AppMountParameters) { - const { renderApp, History } = await import('./app'); + const { renderApp, History } = await import('./todo/app'); return renderApp(params, { appInstanceId: '1', appTitle: 'Routing with browser history', @@ -34,10 +36,10 @@ export class StateContainersExamplesPlugin implements Plugin { }, }); core.application.register({ - id: 'state-containers-example-hash-history', + id: 'stateContainersExampleHashHistory', title: 'State containers example - hash history routing', async mount(params: AppMountParameters) { - const { renderApp, History } = await import('./app'); + const { renderApp, History } = await import('./todo/app'); return renderApp(params, { appInstanceId: '2', appTitle: 'Routing with hash history', @@ -45,6 +47,19 @@ export class StateContainersExamplesPlugin implements Plugin { }); }, }); + + core.application.register({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./with_data_services/application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart as AppPluginDependencies, params); + }, + }); } public start() {} diff --git a/examples/state_containers_examples/public/app.tsx b/examples/state_containers_examples/public/todo/app.tsx similarity index 100% rename from examples/state_containers_examples/public/app.tsx rename to examples/state_containers_examples/public/todo/app.tsx diff --git a/examples/state_containers_examples/public/todo.tsx b/examples/state_containers_examples/public/todo/todo.tsx similarity index 98% rename from examples/state_containers_examples/public/todo.tsx rename to examples/state_containers_examples/public/todo/todo.tsx index 84f64f99d0179..c0617620bde53 100644 --- a/examples/state_containers_examples/public/todo.tsx +++ b/examples/state_containers_examples/public/todo/todo.tsx @@ -42,14 +42,14 @@ import { syncStates, getStateFromKbnUrl, BaseState, -} from '../../../src/plugins/kibana_utils/public'; -import { useUrlTracker } from '../../../src/plugins/kibana_react/public'; +} from '../../../../src/plugins/kibana_utils/public'; +import { useUrlTracker } from '../../../../src/plugins/kibana_react/public'; import { defaultState, pureTransitions, TodoActions, TodoState, -} from '../../../src/plugins/kibana_utils/demos/state_containers/todomvc'; +} from '../../../../src/plugins/kibana_utils/demos/state_containers/todomvc'; interface GlobalState { text: string; diff --git a/examples/state_containers_examples/public/with_data_services/application.tsx b/examples/state_containers_examples/public/with_data_services/application.tsx new file mode 100644 index 0000000000000..1de3cbbc5f988 --- /dev/null +++ b/examples/state_containers_examples/public/with_data_services/application.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { createBrowserHistory } from 'history'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { AppPluginDependencies } from './types'; +import { StateDemoApp } from './components/app'; +import { createKbnUrlStateStorage } from '../../../../src/plugins/kibana_utils/public/'; + +export const renderApp = ( + { notifications, http }: CoreStart, + { navigation, data }: AppPluginDependencies, + { appBasePath, element }: AppMountParameters +) => { + const history = createBrowserHistory({ basename: appBasePath }); + const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx new file mode 100644 index 0000000000000..480afe6c8fe58 --- /dev/null +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -0,0 +1,244 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { History } from 'history'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { Router } from 'react-router-dom'; + +import { + EuiFieldText, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, +} from '@elastic/eui'; + +import { CoreStart } from '../../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; +import { + DataPublicPluginStart, + esFilters, + IIndexPattern, + syncAppFilters, + syncQuery, + QueryState, +} from '../../../../../src/plugins/data/public'; +import { + BaseStateContainer, + createStateContainer, + createStateContainerReactHelpers, + IKbnUrlStateStorage, + syncState, +} from '../../../../../src/plugins/kibana_utils/public'; +import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; + +interface StateDemoAppDeps { + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + history: History; + kbnUrlStateStorage: IKbnUrlStateStorage; +} + +interface AppState { + name: string; + appFilters: esFilters.Filter[]; +} +const defaultAppState: AppState = { + name: '', + appFilters: [], +}; +const _appStateContainer = createStateContainer(defaultAppState); +const { + Provider: AppStateContainerProvider, + useState: useAppState, + useContainer: useAppStateContainer, +} = createStateContainerReactHelpers(); + +interface GlobalState extends QueryState { + globalData: string; +} +const defaultGlobalState: GlobalState = { + globalData: '', +}; +const _globalStateContainer = createStateContainer(defaultGlobalState); +const { + Provider: GlobalStateContainerProvider, + useState: useGlobalState, + useContainer: useGlobalStateContainer, +} = createStateContainerReactHelpers(); + +const App = ({ + notifications, + http, + navigation, + data, + history, + kbnUrlStateStorage, +}: StateDemoAppDeps) => { + const appStateContainer = useAppStateContainer(); + const appState = useAppState(); + + const globalStateContainer = useGlobalStateContainer(); + const globalState = useGlobalState(); + + useStateSyncing(appStateContainer, globalStateContainer, data, kbnUrlStateStorage); + + const indexPattern = useIndexPattern(data); + if (!indexPattern) return
Loading...
; + + // Render the application DOM. + // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ +

+
+
+ appStateContainer.set({ ...appState, name: e.target.value })} + aria-label="My name" + /> + + + globalStateContainer.set({ ...globalState, globalData: e.target.value }) + } + aria-label="My global data" + /> +
+
+
+ +
+
+ ); +}; + +export const StateDemoApp = (props: StateDemoAppDeps) => { + return ( + + + + + + ); +}; + +function useIndexPattern(data: DataPublicPluginStart) { + const [indexPattern, setIndexPattern] = useState(); + useEffect(() => { + const fetchIndexPattern = async () => { + const defaultIndexPattern = await data.indexPatterns.getDefault(); + if (defaultIndexPattern) { + setIndexPattern(defaultIndexPattern); + } + }; + fetchIndexPattern(); + }, [data.indexPatterns]); + + return indexPattern; +} + +function useStateSyncing< + AppState extends { appFilters: esFilters.Filter[] }, + GlobalState extends QueryState +>( + appStateContainer: BaseStateContainer, + globalStateContainer: BaseStateContainer, + data: DataPublicPluginStart, + kbnUrlStateStorage: IKbnUrlStateStorage +) { + // setup sync state utils + useEffect(() => { + // sync global filters, time filters, refresh interval + const stopSyncingQueryStateWithStateContainer = syncQuery(data.query, globalStateContainer); + + // sync app filters wit app state container + const stopSyncingAppFiltersWithStateContainer = syncAppFilters(data.query, appStateContainer); + + // sync app state container with url + const { start: startSyncingAppStateWithUrl, stop: stopSyncingAppStateWithUrl } = syncState({ + storageKey: '_a', + stateStorage: kbnUrlStateStorage, + stateContainer: { + ...appStateContainer, + set: state => state && appStateContainer.set(state), + }, + }); + // sync global state container with url + const { + start: startSyncingGlobalStateWithUrl, + stop: stopSyncingGlobalStateWithUrl, + } = syncState({ + storageKey: '_g', + stateStorage: kbnUrlStateStorage, + stateContainer: { + ...globalStateContainer, + set: state => state && globalStateContainer.set(state), + }, + }); + + startSyncingAppStateWithUrl(); + startSyncingGlobalStateWithUrl(); + + return () => { + stopSyncingQueryStateWithStateContainer(); + stopSyncingAppFiltersWithStateContainer(); + stopSyncingAppStateWithUrl(); + stopSyncingGlobalStateWithUrl(); + }; + }, [data.query, kbnUrlStateStorage, appStateContainer, globalStateContainer]); +} diff --git a/examples/state_containers_examples/public/with_data_services/types.ts b/examples/state_containers_examples/public/with_data_services/types.ts new file mode 100644 index 0000000000000..c63074a7a3810 --- /dev/null +++ b/examples/state_containers_examples/public/with_data_services/types.ts @@ -0,0 +1,31 @@ +/* + * 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 { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StateDemoPublicPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StateDemoPublicPluginStart {} + +export interface AppPluginDependencies { + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; +} diff --git a/examples/state_containers_examples/server/index.ts b/examples/state_containers_examples/server/index.ts new file mode 100644 index 0000000000000..51005d78462a2 --- /dev/null +++ b/examples/state_containers_examples/server/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { PluginInitializerContext } from '../../../src/core/server'; +import { StateDemoServerPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new StateDemoServerPlugin(initializerContext); +} + +export { StateDemoServerPlugin as Plugin }; +export * from '../common'; diff --git a/examples/state_containers_examples/server/plugin.ts b/examples/state_containers_examples/server/plugin.ts new file mode 100644 index 0000000000000..1c3fa9bfb290e --- /dev/null +++ b/examples/state_containers_examples/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../src/core/server'; + +import { StateDemoPluginSetup, StateDemoPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class StateDemoServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('State_demo: Ssetup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('State_demo: Started'); + return {}; + } + + public stop() {} +} + +export { StateDemoServerPlugin as Plugin }; diff --git a/examples/state_containers_examples/server/routes/index.ts b/examples/state_containers_examples/server/routes/index.ts new file mode 100644 index 0000000000000..f6da48ae62c61 --- /dev/null +++ b/examples/state_containers_examples/server/routes/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { IRouter } from '../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/state_demo/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/examples/state_containers_examples/server/types.ts b/examples/state_containers_examples/server/types.ts new file mode 100644 index 0000000000000..6acfc27bd681b --- /dev/null +++ b/examples/state_containers_examples/server/types.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StateDemoPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StateDemoPluginStart {} diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index 7eefda0d0aec1..e3a1f1c08093f 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { syncQuery } from './sync_query'; +export { syncQuery, QueryState } from './sync_query'; export { syncAppFilters } from './sync_app_filters'; diff --git a/src/plugins/data/public/query/state_sync/sync_app_filters.ts b/src/plugins/data/public/query/state_sync/sync_app_filters.ts index 7954729cd8665..ee0bdf42efabe 100644 --- a/src/plugins/data/public/query/state_sync/sync_app_filters.ts +++ b/src/plugins/data/public/query/state_sync/sync_app_filters.ts @@ -21,20 +21,20 @@ import _ from 'lodash'; import { filter, map } from 'rxjs/operators'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { esFilters } from '../../../common'; -import { FilterManager } from '../filter_manager'; import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; +import { QueryStart } from '../query_service'; /** * Helper utility to sync application's state filters, with filter manager * @param filterManager * @param appState */ -export function syncAppFilters( - filterManager: FilterManager, - appState: BaseStateContainer +export function syncAppFilters( + { filterManager }: QueryStart, + appState: BaseStateContainer ) { // make sure initial app filters are picked by filterManager - filterManager.setAppFilters(_.cloneDeep(appState.get())); + filterManager.setAppFilters(_.cloneDeep(appState.get().appFilters)); const subs = [ filterManager @@ -43,18 +43,24 @@ export function syncAppFilters( map(() => filterManager.getAppFilters()), filter( // continue only if app state filters updated - appFilters => !compareFilters(appFilters, appState.get(), COMPARE_ALL_OPTIONS) + appFilters => !compareFilters(appFilters, appState.get().appFilters, COMPARE_ALL_OPTIONS) ) ) .subscribe(appFilters => { - appState.set(appFilters); + appState.set({ ...appState.get(), appFilters } as S); }), // if appFilters in dashboardStateManager changed (e.g browser history update), // sync it to filterManager appState.state$.subscribe(() => { - if (!compareFilters(appState.get(), filterManager.getAppFilters(), COMPARE_ALL_OPTIONS)) { - filterManager.setAppFilters(_.cloneDeep(appState.get())); + if ( + !compareFilters( + appState.get().appFilters, + filterManager.getAppFilters(), + COMPARE_ALL_OPTIONS + ) + ) { + filterManager.setAppFilters(_.cloneDeep(appState.get().appFilters)); } }), ]; diff --git a/src/plugins/data/public/query/state_sync/sync_query.ts b/src/plugins/data/public/query/state_sync/sync_query.ts index be641e89f9b76..cb82351a1ee8a 100644 --- a/src/plugins/data/public/query/state_sync/sync_query.ts +++ b/src/plugins/data/public/query/state_sync/sync_query.ts @@ -20,77 +20,35 @@ import { Subscription } from 'rxjs'; import _ from 'lodash'; import { filter, map } from 'rxjs/operators'; -import { - createStateContainer, - IKbnUrlStateStorage, - syncState, -} from '../../../../kibana_utils/public'; +import { BaseStateContainer } from '../../../../kibana_utils/public'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { esFilters, RefreshInterval, TimeRange } from '../../../common'; import { QueryStart } from '../query_service'; -const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export interface QuerySyncState { +export interface QueryState { time?: TimeRange; refreshInterval?: RefreshInterval; filters?: esFilters.Filter[]; } /** - * Helper utility to set up syncing between query services and url's '_g' query param + * Helper utility to sync global data from query services with state container + * @param filterManager + * @param appState */ -export const syncQuery = ( +export const syncQuery = ( { timefilter: { timefilter }, filterManager }: QueryStart, - urlStateStorage: IKbnUrlStateStorage + globalState: BaseStateContainer ) => { - const defaultState: QuerySyncState = { - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - filters: filterManager.getGlobalFilters(), - }; - - // retrieve current state from `_g` url - const initialStateFromUrl = urlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); - - // remember whether there were info in the URL - const hasInheritedQueryFromUrl = Boolean( - initialStateFromUrl && Object.keys(initialStateFromUrl).length - ); - - // prepare initial state, whatever was in URL takes precedences over current state in services - const initialState: QuerySyncState = { - ...defaultState, - ...initialStateFromUrl, - }; - - // create state container, which will be used for syncing with syncState() util - const querySyncStateContainer = createStateContainer( - initialState, - { - setTime: (state: QuerySyncState) => (time: TimeRange) => ({ ...state, time }), - setRefreshInterval: (state: QuerySyncState) => (refreshInterval: RefreshInterval) => ({ - ...state, - refreshInterval, - }), - setFilters: (state: QuerySyncState) => (filters: esFilters.Filter[]) => ({ - ...state, - filters, - }), - }, - { - time: (state: QuerySyncState) => () => state.time, - refreshInterval: (state: QuerySyncState) => () => state.refreshInterval, - filters: (state: QuerySyncState) => () => state.filters, - } - ); - const subs: Subscription[] = [ timefilter.getTimeUpdate$().subscribe(() => { - querySyncStateContainer.transitions.setTime(timefilter.getTime()); + globalState.set({ ...globalState.get(), time: timefilter.getTime() } as S); }), timefilter.getRefreshIntervalUpdate$().subscribe(() => { - querySyncStateContainer.transitions.setRefreshInterval(timefilter.getRefreshInterval()); + globalState.set({ + ...globalState.get(), + refreshInterval: timefilter.getRefreshInterval(), + } as S); }), filterManager .getUpdates$() @@ -99,7 +57,7 @@ export const syncQuery = ( filter(newGlobalFilters => { // continue only if global filters changed // and ignore app state filters - const oldGlobalFilters = querySyncStateContainer.get().filters; + const oldGlobalFilters = globalState.get().filters; return ( !oldGlobalFilters || !compareFilters(newGlobalFilters, oldGlobalFilters, COMPARE_ALL_OPTIONS) @@ -107,61 +65,29 @@ export const syncQuery = ( }) ) .subscribe(newGlobalFilters => { - querySyncStateContainer.transitions.setFilters(newGlobalFilters); + globalState.set({ ...globalState.get(), filters: newGlobalFilters } as S); }), - querySyncStateContainer.state$.subscribe( - ({ time, filters: globalFilters, refreshInterval }) => { - // cloneDeep is required because services are mutating passed objects - // and state in state container is frozen - if (time && !_.isEqual(time, timefilter.getTime())) { - timefilter.setTime(_.cloneDeep(time)); - } + globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { + // cloneDeep is required because services are mutating passed objects + // and state in state container is frozen + if (time && !_.isEqual(time, timefilter.getTime())) { + timefilter.setTime(_.cloneDeep(time)); + } - if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { - timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); - } + if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { + timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); + } - if ( - globalFilters && - !compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) - ) { - filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); - } + if ( + globalFilters && + !compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) + ) { + filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); } - ), + }), ]; - // if there weren't any initial state in url, - // then put _g key into url - if (!initialStateFromUrl) { - urlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { - replace: true, - }); - } - - // trigger initial syncing from state container to services if needed - querySyncStateContainer.set(initialState); - - const { start, stop } = syncState({ - stateStorage: urlStateStorage, - stateContainer: { - ...querySyncStateContainer, - set: state => { - if (state) { - // syncState utils requires to handle incoming "null" value - querySyncStateContainer.set(state); - } - }, - }, - storageKey: GLOBAL_STATE_STORAGE_KEY, - }); - - start(); - return { - stop: () => { - subs.forEach(s => s.unsubscribe()); - stop(); - }, - hasInheritedQueryFromUrl, + return () => { + subs.forEach(s => s.unsubscribe()); }; }; diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 6f1be2825dd01..2a9eee8ef8a4a 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -22,7 +22,7 @@ import { Subscription } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../kibana_react/public'; -import { DataPublicPluginStart, esFilters } from '../..'; +import { DataPublicPluginStart, esFilters, Query, TimeRange } from '../..'; import { QueryStart } from '../../query'; import { SearchBarOwnProps, SearchBar } from './search_bar'; @@ -63,6 +63,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) const [refreshPaused, setRefreshPaused] = useState(tfRefreshInterval.pause); const [filters, setFilters] = useState(fmFilters); + const [query, setQuery] = useState({ language: 'kql', query: '' }); // We do not really need to keep track of the time // since this is just for initialization @@ -116,8 +117,15 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) refreshInterval={refreshInterval} isRefreshPaused={refreshPaused} filters={filters} + query={query} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} + onQuerySubmit={(payload: { dateRange: TimeRange; query?: Query }) => { + timefilter.timefilter.setTime(payload.dateRange); + if (payload.query) { + setQuery(payload.query); + } + }} {...props} /> diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index ceaeb24e7fe7c..6f215f112fa30 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -55,6 +55,9 @@ interface SearchBarInjectedDeps { onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; isRefreshPaused?: boolean; refreshInterval?: number; + + // Query bar - should be in SearchBarInjectedDeps + query?: Query; } export interface SearchBarOwnProps { @@ -69,8 +72,6 @@ export interface SearchBarOwnProps { showFilterBar?: boolean; showDatePicker?: boolean; showAutoRefreshOnly?: boolean; - // Query bar - should be in SearchBarInjectedDeps - query?: Query; // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; From 8201cbde0c6cb3618bc952c40fc9a7bfae6a2635 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jan 2020 12:59:17 +0100 Subject: [PATCH 02/29] wip --- .../with_data_services/components/app.tsx | 77 ++++++++----- .../np_ready/dashboard_app_controller.tsx | 17 +-- .../public/dashboard/np_ready/legacy_app.js | 4 +- ...s.test.ts => connect_to_app_state.test.ts} | 102 +++++++++++------- ...app_filters.ts => connect_to_app_state.ts} | 40 ++++--- ...nc_query.ts => connect_to_global_state.ts} | 35 ++++-- .../data/public/query/state_sync/index.ts | 5 +- ....ts => sync_global_state_with_url.test.ts} | 30 +++--- .../state_sync/sync_global_state_with_url.ts | 96 +++++++++++++++++ .../public/query/timefilter/timefilter.ts | 13 +++ 10 files changed, 304 insertions(+), 115 deletions(-) rename src/plugins/data/public/query/state_sync/{sync_app_filters.test.ts => connect_to_app_state.test.ts} (61%) rename src/plugins/data/public/query/state_sync/{sync_app_filters.ts => connect_to_app_state.ts} (60%) rename src/plugins/data/public/query/state_sync/{sync_query.ts => connect_to_global_state.ts} (72%) rename src/plugins/data/public/query/state_sync/{sync_query.test.ts => sync_global_state_with_url.test.ts} (82%) create mode 100644 src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 480afe6c8fe58..8539e7a70005b 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { History } from 'history'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; @@ -36,18 +36,20 @@ import { import { CoreStart } from '../../../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { + connectToQueryAppState, + connectToQueryGlobalState, DataPublicPluginStart, - esFilters, IIndexPattern, - syncAppFilters, - syncQuery, - QueryState, + QueryAppState, + QueryGlobalState, } from '../../../../../src/plugins/data/public'; import { + BaseState, BaseStateContainer, createStateContainer, createStateContainerReactHelpers, IKbnUrlStateStorage, + ReduxLikeStateContainer, syncState, } from '../../../../../src/plugins/kibana_utils/public'; import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; @@ -61,33 +63,29 @@ interface StateDemoAppDeps { kbnUrlStateStorage: IKbnUrlStateStorage; } -interface AppState { +interface AppState extends QueryAppState { name: string; - appFilters: esFilters.Filter[]; } const defaultAppState: AppState = { name: '', - appFilters: [], }; -const _appStateContainer = createStateContainer(defaultAppState); const { Provider: AppStateContainerProvider, useState: useAppState, useContainer: useAppStateContainer, -} = createStateContainerReactHelpers(); +} = createStateContainerReactHelpers>(); -interface GlobalState extends QueryState { +interface GlobalState extends QueryGlobalState { globalData: string; } const defaultGlobalState: GlobalState = { globalData: '', }; -const _globalStateContainer = createStateContainer(defaultGlobalState); const { Provider: GlobalStateContainerProvider, useState: useGlobalState, useContainer: useGlobalStateContainer, -} = createStateContainerReactHelpers(); +} = createStateContainerReactHelpers>(); const App = ({ notifications, @@ -168,15 +166,28 @@ const App = ({ }; export const StateDemoApp = (props: StateDemoAppDeps) => { + const appStateContainer = useCreateStateContainer(defaultAppState); + const globalStateContainer = useCreateStateContainer(defaultGlobalState); + return ( - - + + ); }; +function useCreateStateContainer( + defaultState: State +): ReduxLikeStateContainer { + const stateContainerRef = useRef | null>(null); + if (!stateContainerRef.current) { + stateContainerRef.current = createStateContainer(defaultState); + } + return stateContainerRef.current; +} + function useIndexPattern(data: DataPublicPluginStart) { const [indexPattern, setIndexPattern] = useState(); useEffect(() => { @@ -192,10 +203,7 @@ function useIndexPattern(data: DataPublicPluginStart) { return indexPattern; } -function useStateSyncing< - AppState extends { appFilters: esFilters.Filter[] }, - GlobalState extends QueryState ->( +function useStateSyncing( appStateContainer: BaseStateContainer, globalStateContainer: BaseStateContainer, data: DataPublicPluginStart, @@ -204,10 +212,16 @@ function useStateSyncing< // setup sync state utils useEffect(() => { // sync global filters, time filters, refresh interval - const stopSyncingQueryStateWithStateContainer = syncQuery(data.query, globalStateContainer); + const stopSyncingQueryGlobalStateWithStateContainer = connectToQueryGlobalState( + data.query, + globalStateContainer + ); - // sync app filters wit app state container - const stopSyncingAppFiltersWithStateContainer = syncAppFilters(data.query, appStateContainer); + // sync app filters with app state container + const stopSyncingQueryAppStateWithStateContainer = connectToQueryAppState( + data.query, + appStateContainer + ); // sync app state container with url const { start: startSyncingAppStateWithUrl, stop: stopSyncingAppStateWithUrl } = syncState({ @@ -231,12 +245,27 @@ function useStateSyncing< }, }); + const initialAppState: AppState = { + ...appStateContainer.get(), + ...kbnUrlStateStorage.get('_a'), + }; + appStateContainer.set(initialAppState); + + const initialGlobalState: GlobalState = { + ...globalStateContainer.get(), + ...kbnUrlStateStorage.get('_g'), + }; + globalStateContainer.set(initialGlobalState); + + kbnUrlStateStorage.set('_a', initialAppState); + kbnUrlStateStorage.set('_g', initialGlobalState); + startSyncingAppStateWithUrl(); startSyncingGlobalStateWithUrl(); return () => { - stopSyncingQueryStateWithStateContainer(); - stopSyncingAppFiltersWithStateContainer(); + stopSyncingQueryGlobalStateWithStateContainer(); + stopSyncingQueryAppStateWithStateContainer(); stopSyncingAppStateWithUrl(); stopSyncingGlobalStateWithUrl(); }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index e85054cd7fb34..8e3bd80a9d377 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -41,8 +41,8 @@ import { IndexPatternsContract, Query, SavedQuery, - syncAppFilters, - syncQuery, + syncGlobalQueryStateWithUrl, + connectToQueryAppState, } from '../../../../../../plugins/data/public'; import { @@ -122,7 +122,7 @@ export class DashboardAppController { const { stop: stopSyncingGlobalStateWithUrl, hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl, - } = syncQuery(queryService, kbnUrlStateStorage); + } = syncGlobalQueryStateWithUrl(queryService, kbnUrlStateStorage); let lastReloadRequestTime = 0; @@ -139,10 +139,13 @@ export class DashboardAppController { history, }); - const stopSyncingAppFilters = syncAppFilters(filterManager, { - set: filters => dashboardStateManager.setFilters(filters), - get: () => dashboardStateManager.appState.filters, - state$: dashboardStateManager.appState$.pipe(map(state => state.filters)), + // sync initial app filters from state to filterManager + filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters)); + // setup syncing of app filters between appState and filterManager + const stopSyncingAppFilters = connectToQueryAppState(queryService, { + set: ({ filters }) => dashboardStateManager.setFilters(filters || []), + get: () => ({ filters: dashboardStateManager.appState.filters }), + state$: dashboardStateManager.appState$.pipe(map(state => ({ filters: state.filters }))), }); // The hash check is so we only update the time filter on dashboard open, not during diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 7ba404d52d9a6..088255c94f7fe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -31,7 +31,7 @@ import { } from '../../../../../../plugins/kibana_utils/public'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; -import { syncQuery } from '../../../../../../plugins/data/public'; +import { syncGlobalQueryStateWithUrl } from '../../../../../../plugins/data/public'; export function initDashboardApp(app, deps) { initDashboardAppDirective(app, deps); @@ -93,7 +93,7 @@ export function initDashboardApp(app, deps) { const dashboardConfig = deps.dashboardConfig; // syncs `_g` portion of url with query services - const { stop: stopSyncingGlobalStateWithUrl } = syncQuery( + const { stop: stopSyncingGlobalStateWithUrl } = syncGlobalQueryStateWithUrl( deps.npDataStart.query, deps.kbnUrlStateStorage ); diff --git a/src/plugins/data/public/query/state_sync/sync_app_filters.test.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts similarity index 61% rename from src/plugins/data/public/query/state_sync/sync_app_filters.test.ts rename to src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts index 61270ecc09979..44ecc58c7bf70 100644 --- a/src/plugins/data/public/query/state_sync/sync_app_filters.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts @@ -21,19 +21,32 @@ import { Subscription } from 'rxjs'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; import { esFilters } from '../../../common'; -import { syncAppFilters } from './sync_app_filters'; +import { connectToQueryAppState } from './connect_to_app_state'; import { coreMock } from '../../../../../core/public/mocks'; -import { BaseStateContainer, createStateContainer } from '../../../../kibana_utils/public'; +import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; +import { QueryService, QueryStart } from '../query_service'; +import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; const setupMock = coreMock.createSetup(); +const startMock = coreMock.createStart(); setupMock.uiSettings.get.mockImplementation((key: string) => { - return true; + switch (key) { + case 'filters:pinnedByDefault': + return true; + case 'timepicker:timeDefaults': + return { from: 'now-15m', to: 'now' }; + case 'timepicker:refreshIntervalDefaults': + return { pause: false, value: 0 }; + default: + throw new Error(`sync_query test: not mocked uiSetting: ${key}`); + } }); -describe('sync_app_filters', () => { +describe('connect_to_app_state', () => { + let queryServiceStart: QueryStart; let filterManager: FilterManager; - let appState: BaseStateContainer; + let appState: BaseStateContainer<{ filters: esFilters.Filter[] }>; let appStateSub: Subscription; let appStateChangeTriggered = jest.fn(); let filterManagerChangeSub: Subscription; @@ -45,8 +58,15 @@ describe('sync_app_filters', () => { let aF2: esFilters.Filter; beforeEach(() => { - filterManager = new FilterManager(setupMock.uiSettings); - appState = createStateContainer([] as esFilters.Filter[]); + const queryService = new QueryService(); + queryService.setup({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + }); + queryServiceStart = queryService.start(startMock.savedObjects); + filterManager = queryServiceStart.filterManager; + + appState = createStateContainer({ filters: [] as esFilters.Filter[] }); appStateChangeTriggered = jest.fn(); appStateSub = appState.state$.subscribe(appStateChangeTriggered); @@ -65,93 +85,96 @@ describe('sync_app_filters', () => { describe('sync from filterManager to app state', () => { test('should sync app filters to app state when new app filters set to filterManager', () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); filterManager.setFilters([gF1, aF1]); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); stop(); }); test('should not sync global filters to app state ', () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); filterManager.setFilters([gF1, gF2]); - expect(appState.get()).toHaveLength(0); + expect(appState.get().filters).toHaveLength(0); stop(); }); test("should not trigger changes when app filters didn't change", () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); filterManager.setFilters([gF1, aF1]); - filterManager.setFilters([gF2, aF1]); expect(appStateChangeTriggered).toBeCalledTimes(1); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); stop(); }); test('should trigger changes when app filters change', () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); filterManager.setFilters([gF1, aF1]); filterManager.setFilters([gF1, aF2]); expect(appStateChangeTriggered).toBeCalledTimes(2); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); stop(); }); test('resetting filters should sync to app state', () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); filterManager.setFilters([gF1, aF1]); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); filterManager.removeAll(); - expect(appState.get()).toHaveLength(0); + expect(appState.get().filters).toHaveLength(0); stop(); }); test("shouldn't sync filters when syncing is stopped", () => { - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); filterManager.setFilters([gF1, aF1]); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); stop(); filterManager.removeAll(); - expect(appState.get()).toHaveLength(1); + expect(appState.get().filters).toHaveLength(1); }); - }); - describe('sync from app state to filterManager', () => { - test('should pick up initial state from app state', () => { - appState.set([aF1]); + + test('should pick up initial state from filterManager', () => { + appState.set({ filters: [aF1] }); filterManager.setFilters([gF1]); - const stop = syncAppFilters(filterManager, appState); - expect(filterManager.getFilters()).toHaveLength(2); + appStateChangeTriggered.mockClear(); + const stop = connectToQueryAppState(queryServiceStart, appState); expect(appStateChangeTriggered).toBeCalledTimes(1); + expect(appState.get().filters).toHaveLength(0); stop(); }); - + }); + describe('sync from app state to filterManager', () => { test('changes to app state should be synced to app filters', () => { filterManager.setFilters([gF1]); - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); - appState.set([aF1]); + appState.set({ filters: [aF1] }); expect(filterManager.getFilters()).toHaveLength(2); expect(filterManager.getAppFilters()).toHaveLength(1); @@ -162,9 +185,10 @@ describe('sync_app_filters', () => { test('global filters should remain untouched', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = syncAppFilters(filterManager, appState); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); - appState.set([]); + appState.set({ filters: [] }); expect(filterManager.getFilters()).toHaveLength(2); expect(filterManager.getGlobalFilters()).toHaveLength(2); @@ -176,9 +200,9 @@ describe('sync_app_filters', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); filterManagerChangeTriggered.mockClear(); - appState.set([aF1, aF2]); - const stop = syncAppFilters(filterManager, appState); - appState.set([aF1, aF2]); + appState.set({ filters: [aF1, aF2] }); + const stop = connectToQueryAppState(queryServiceStart, appState); + appState.set({ filters: [aF1, aF2] }); expect(filterManagerChangeTriggered).toBeCalledTimes(0); stop(); @@ -186,11 +210,11 @@ describe('sync_app_filters', () => { test('stop() should stop syncing', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = syncAppFilters(filterManager, appState); - appState.set([]); + const stop = connectToQueryAppState(queryServiceStart, appState); + appState.set({ filters: [] }); expect(filterManager.getFilters()).toHaveLength(2); stop(); - appState.set([aF1]); + appState.set({ filters: [aF1] }); expect(filterManager.getFilters()).toHaveLength(2); }); }); diff --git a/src/plugins/data/public/query/state_sync/sync_app_filters.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts similarity index 60% rename from src/plugins/data/public/query/state_sync/sync_app_filters.ts rename to src/plugins/data/public/query/state_sync/connect_to_app_state.ts index ee0bdf42efabe..ba6262b187be8 100644 --- a/src/plugins/data/public/query/state_sync/sync_app_filters.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -24,18 +24,28 @@ import { esFilters } from '../../../common'; import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; import { QueryStart } from '../query_service'; +export interface QueryAppState { + filters?: esFilters.Filter[]; +} + /** - * Helper utility to sync application's state filters, with filter manager - * @param filterManager - * @param appState + * Helper utility to sync app state data from query services: app filters (not pinned) + * with state container + * @param QueryStart + * @param stateContainer */ -export function syncAppFilters( +export function connectToQueryAppState( { filterManager }: QueryStart, appState: BaseStateContainer ) { - // make sure initial app filters are picked by filterManager - filterManager.setAppFilters(_.cloneDeep(appState.get().appFilters)); + // initial syncing + // TODO: + // filterManager takes precedence, this seems like a good default, + // and apps could anyway set their own value after initialisation, + // but maybe maybe this should be a configurable option? + appState.set({ ...appState.get(), filters: filterManager.getAppFilters() } as S); + // subscribe to updates const subs = [ filterManager .getUpdates$() @@ -43,24 +53,20 @@ export function syncAppFilters( map(() => filterManager.getAppFilters()), filter( // continue only if app state filters updated - appFilters => !compareFilters(appFilters, appState.get().appFilters, COMPARE_ALL_OPTIONS) + appFilters => + !compareFilters(appFilters, appState.get().filters || [], COMPARE_ALL_OPTIONS) ) ) .subscribe(appFilters => { - appState.set({ ...appState.get(), appFilters } as S); + appState.set({ ...appState.get(), filters: appFilters } as S); }), // if appFilters in dashboardStateManager changed (e.g browser history update), // sync it to filterManager - appState.state$.subscribe(() => { - if ( - !compareFilters( - appState.get().appFilters, - filterManager.getAppFilters(), - COMPARE_ALL_OPTIONS - ) - ) { - filterManager.setAppFilters(_.cloneDeep(appState.get().appFilters)); + appState.state$.pipe(map(state => state.filters)).subscribe(appFilters => { + appFilters = appFilters || []; + if (!compareFilters(appFilters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS)) { + filterManager.setAppFilters(_.cloneDeep(appFilters)); } }), ]; diff --git a/src/plugins/data/public/query/state_sync/sync_query.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts similarity index 72% rename from src/plugins/data/public/query/state_sync/sync_query.ts rename to src/plugins/data/public/query/state_sync/connect_to_global_state.ts index cb82351a1ee8a..4ebfa08d69728 100644 --- a/src/plugins/data/public/query/state_sync/sync_query.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -25,21 +25,34 @@ import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compa import { esFilters, RefreshInterval, TimeRange } from '../../../common'; import { QueryStart } from '../query_service'; -export interface QueryState { +export interface QueryGlobalState { time?: TimeRange; refreshInterval?: RefreshInterval; filters?: esFilters.Filter[]; } /** - * Helper utility to sync global data from query services with state container - * @param filterManager - * @param appState + * Helper utility to sync global data from query services: time, refreshInterval, global (pinned) filters + * with state container + * @param QueryStart + * @param stateContainer */ -export const syncQuery = ( +export const connectToQueryGlobalState = ( { timefilter: { timefilter }, filterManager }: QueryStart, globalState: BaseStateContainer ) => { + // initial syncing + // TODO: + // data services take precedence, this seems like a good default, + // and apps could anyway set their own value after initialisation, + // but maybe maybe this should be a configurable option? + globalState.set({ + ...globalState.get(), + filters: filterManager.getGlobalFilters(), + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + } as S); + const subs: Subscription[] = [ timefilter.getTimeUpdate$().subscribe(() => { globalState.set({ ...globalState.get(), time: timefilter.getTime() } as S); @@ -70,18 +83,18 @@ export const syncQuery = ( globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { // cloneDeep is required because services are mutating passed objects // and state in state container is frozen - if (time && !_.isEqual(time, timefilter.getTime())) { + time = time || timefilter.getTimeDefaults(); + if (!_.isEqual(time, timefilter.getTime())) { timefilter.setTime(_.cloneDeep(time)); } - if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { + refreshInterval = refreshInterval || timefilter.getRefreshIntervalDefaults(); + if (!_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); } - if ( - globalFilters && - !compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) - ) { + globalFilters = globalFilters || []; + if (!compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)) { filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); } }), diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index e3a1f1c08093f..fe18ffec2bdd2 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export { syncQuery, QueryState } from './sync_query'; -export { syncAppFilters } from './sync_app_filters'; +export { connectToQueryGlobalState, QueryGlobalState } from './connect_to_global_state'; +export { connectToQueryAppState, QueryAppState } from './connect_to_app_state'; +export { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; diff --git a/src/plugins/data/public/query/state_sync/sync_query.test.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts similarity index 82% rename from src/plugins/data/public/query/state_sync/sync_query.test.ts rename to src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts index 0973af13cacd5..3bafd49b0b766 100644 --- a/src/plugins/data/public/query/state_sync/sync_query.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts @@ -31,7 +31,8 @@ import { import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { TimefilterContract } from '../timefilter'; -import { QuerySyncState, syncQuery } from './sync_query'; +import { QueryGlobalState } from './connect_to_global_state'; +import { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -49,7 +50,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { } }); -describe('sync_query', () => { +describe('sync_global_state_with_url', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; let timefilter: TimefilterContract; @@ -90,7 +91,7 @@ describe('sync_query', () => { }); test('url is actually changed when data in services changes', () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); kbnUrlStateStorage.flush(); // sync force location change expect(history.location.hash).toMatchInlineSnapshot( @@ -100,16 +101,16 @@ describe('sync_query', () => { }); test('when filters change, global filters synced to urlStorage', () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); - expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); + expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); stop(); }); test('when time range changes, time synced to urlStorage', () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setTime({ from: 'now-30m', to: 'now' }); - expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ from: 'now-30m', to: 'now', }); @@ -117,9 +118,9 @@ describe('sync_query', () => { }); test('when refresh interval changes, refresh interval is synced to urlStorage', () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setRefreshInterval({ pause: true, value: 100 }); - expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ pause: true, value: 100, }); @@ -127,7 +128,7 @@ describe('sync_query', () => { }); test('when url is changed, filters synced back to filterManager', () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); kbnUrlStateStorage.cancel(); // stop initial syncing pending update history.push(pathWithFilter); expect(filterManager.getGlobalFilters()).toHaveLength(1); @@ -137,14 +138,17 @@ describe('sync_query', () => { test('initial url should be synced with services', () => { history.push(pathWithFilter); - const { stop, hasInheritedQueryFromUrl } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop, hasInheritedQueryFromUrl } = syncGlobalQueryStateWithUrl( + queryServiceStart, + kbnUrlStateStorage + ); expect(hasInheritedQueryFromUrl).toBe(true); expect(filterManager.getGlobalFilters()).toHaveLength(1); stop(); }); test("url changes shouldn't trigger services updates if data didn't change", () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManagerChangeTriggered.mockClear(); history.push(pathWithFilter); @@ -156,7 +160,7 @@ describe('sync_query', () => { }); test("if data didn't change, kbnUrlStateStorage.set shouldn't be called", () => { - const { stop } = syncQuery(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); const spy = jest.spyOn(kbnUrlStateStorage, 'set'); filterManager.setFilters([gF]); // global filters didn't change diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts new file mode 100644 index 0000000000000..3bde0a966c5dc --- /dev/null +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts @@ -0,0 +1,96 @@ +/* + * 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 { + IKbnUrlStateStorage, + syncState, + createStateContainer, +} from '../../../../kibana_utils/public'; +import { QueryStart } from '../query_service'; +import { connectToQueryGlobalState, QueryGlobalState } from './connect_to_global_state'; + +const GLOBAL_STATE_STORAGE_KEY = '_g'; + +/** + * Helper utility to sync global data from query services with url ('_g' query param) + * @param QueryStart + * @param kbnUrlStateStorage - url storage to use + */ +export const syncGlobalQueryStateWithUrl = ( + query: QueryStart, + kbnUrlStateStorage: IKbnUrlStateStorage +) => { + const { + timefilter: { timefilter }, + filterManager, + } = query; + const defaultState: QueryGlobalState = { + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + filters: filterManager.getGlobalFilters(), + }; + + // retrieve current state from `_g` url + const initialStateFromUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); + + // remember whether there were info in the URL + const hasInheritedQueryFromUrl = Boolean( + initialStateFromUrl && Object.keys(initialStateFromUrl).length + ); + + // prepare initial state, whatever was in URL takes precedences over current state in services + const initialState: QueryGlobalState = { + ...defaultState, + ...initialStateFromUrl, + }; + + const globalQueryStateContainer = createStateContainer(initialState); + const stopSyncingWithStateContainer = connectToQueryGlobalState(query, globalQueryStateContainer); + + // if there weren't any initial state in url, + // then put _g key into url + if (!initialStateFromUrl) { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { + replace: true, + }); + } + + // trigger initial syncing from state container to services if needed + globalQueryStateContainer.set(initialState); + + const { start, stop: stopSyncingWithUrl } = syncState({ + stateStorage: kbnUrlStateStorage, + stateContainer: { + ...globalQueryStateContainer, + set: state => { + globalQueryStateContainer.set(state || defaultState); + }, + }, + storageKey: GLOBAL_STATE_STORAGE_KEY, + }); + + start(); + return { + stop: () => { + stopSyncingWithStateContainer(); + stopSyncingWithUrl(); + }, + hasInheritedQueryFromUrl, + }; +}; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 58806a9328b1c..4fbdac47fb3b0 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -50,8 +50,13 @@ export class Timefilter { private _autoRefreshIntervalId: number = 0; + private readonly timeDefaults: TimeRange; + private readonly refreshIntervalDefaults: RefreshInterval; + constructor(config: TimefilterConfig, timeHistory: TimeHistoryContract) { this._history = timeHistory; + this.timeDefaults = config.timeDefaults; + this.refreshIntervalDefaults = config.refreshIntervalDefaults; this._time = config.timeDefaults; this.setRefreshInterval(config.refreshIntervalDefaults); } @@ -208,6 +213,14 @@ export class Timefilter { this.enabledUpdated$.next(false); }; + public getTimeDefaults(): TimeRange { + return _.cloneDeep(this.timeDefaults); + } + + public getRefreshIntervalDefaults(): RefreshInterval { + return _.cloneDeep(this.refreshIntervalDefaults); + } + private getForceNow = () => { const forceNow = parseQueryString().forceNow as string; if (!forceNow) { From e855db865765fdcb80f5afe3a18a237c833cbe8f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jan 2020 13:07:05 +0100 Subject: [PATCH 03/29] improve example --- .../with_data_services/components/app.tsx | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 8539e7a70005b..e0480ec0cd16c 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -42,6 +42,7 @@ import { IIndexPattern, QueryAppState, QueryGlobalState, + QueryStart, } from '../../../../../src/plugins/data/public'; import { BaseState, @@ -101,7 +102,7 @@ const App = ({ const globalStateContainer = useGlobalStateContainer(); const globalState = useGlobalState(); - useStateSyncing(appStateContainer, globalStateContainer, data, kbnUrlStateStorage); + useStateSyncing(appStateContainer, globalStateContainer, data.query, kbnUrlStateStorage); const indexPattern = useIndexPattern(data); if (!indexPattern) return
Loading...
; @@ -203,36 +204,45 @@ function useIndexPattern(data: DataPublicPluginStart) { return indexPattern; } +/** + * Setup syncing of state containers with url + * @param appStateContainer + * @param globalStateContainer + * @param data + * @param kbnUrlStateStorage + */ function useStateSyncing( appStateContainer: BaseStateContainer, globalStateContainer: BaseStateContainer, - data: DataPublicPluginStart, + query: QueryStart, kbnUrlStateStorage: IKbnUrlStateStorage ) { // setup sync state utils useEffect(() => { - // sync global filters, time filters, refresh interval + // sync global filters, time filters, refresh interval from data.query to state container const stopSyncingQueryGlobalStateWithStateContainer = connectToQueryGlobalState( - data.query, + query, globalStateContainer ); - // sync app filters with app state container + // sync app filters with app state container from data.query to state container const stopSyncingQueryAppStateWithStateContainer = connectToQueryAppState( - data.query, + query, appStateContainer ); - // sync app state container with url + // sets up syncing app state container with url const { start: startSyncingAppStateWithUrl, stop: stopSyncingAppStateWithUrl } = syncState({ storageKey: '_a', stateStorage: kbnUrlStateStorage, stateContainer: { ...appStateContainer, + // stateSync utils requires explicit handling of default state ("null") set: state => state && appStateContainer.set(state), }, }); - // sync global state container with url + + // sets up syncing global state container with url const { start: startSyncingGlobalStateWithUrl, stop: stopSyncingGlobalStateWithUrl, @@ -241,25 +251,34 @@ function useStateSyncing state && globalStateContainer.set(state), }, }); + // merge initial state from app state container and current state in url const initialAppState: AppState = { ...appStateContainer.get(), ...kbnUrlStateStorage.get('_a'), }; + // trigger state update. actually needed in case some data was in url appStateContainer.set(initialAppState); + // merge initial state from global state container and current state in url const initialGlobalState: GlobalState = { ...globalStateContainer.get(), ...kbnUrlStateStorage.get('_g'), }; + // trigger state update. actually needed in case some data was in url globalStateContainer.set(initialGlobalState); + // set current url to whatever is in app state container kbnUrlStateStorage.set('_a', initialAppState); + + // set current url to whatever is in global state container kbnUrlStateStorage.set('_g', initialGlobalState); + // finally start syncing state containers with url startSyncingAppStateWithUrl(); startSyncingGlobalStateWithUrl(); @@ -269,5 +288,5 @@ function useStateSyncing Date: Tue, 28 Jan 2020 13:19:46 +0100 Subject: [PATCH 04/29] improve --- .../state_sync/connect_to_global_state.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts index 4ebfa08d69728..65b2e5e6a4aa3 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -66,16 +66,14 @@ export const connectToQueryGlobalState = ( filterManager .getUpdates$() .pipe( - map(() => filterManager.getGlobalFilters()), // we need to track only global filters here - filter(newGlobalFilters => { - // continue only if global filters changed - // and ignore app state filters - const oldGlobalFilters = globalState.get().filters; - return ( - !oldGlobalFilters || - !compareFilters(newGlobalFilters, oldGlobalFilters, COMPARE_ALL_OPTIONS) - ); - }) + // we need to track only global filters here + map(() => filterManager.getGlobalFilters()), + // continue only if global filters changed + // and ignore app state filters + filter( + newGlobalFilters => + !compareFilters(newGlobalFilters, globalState.get().filters || [], COMPARE_ALL_OPTIONS) + ) ) .subscribe(newGlobalFilters => { globalState.set({ ...globalState.get(), filters: newGlobalFilters } as S); From 224841b5ca4ac4733ac1ce74180896f69c461de4 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jan 2020 13:57:52 +0100 Subject: [PATCH 05/29] fix --- examples/state_containers_examples/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 091130487791b..3f43072c2aade 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -9,6 +9,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", + "common/**/*.ts", "../../typings/**/*" ], "exclude": [] From 98703dcc43592fe674d697da7e04b7aee73f1136 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 28 Jan 2020 15:36:25 +0100 Subject: [PATCH 06/29] improve --- .../with_data_services/components/app.tsx | 85 ++++++++++--------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index e0480ec0cd16c..a5e6e912cd73e 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -102,7 +102,8 @@ const App = ({ const globalStateContainer = useGlobalStateContainer(); const globalState = useGlobalState(); - useStateSyncing(appStateContainer, globalStateContainer, data.query, kbnUrlStateStorage); + useGlobalStateSyncing(globalStateContainer, data.query, kbnUrlStateStorage); + useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); const indexPattern = useIndexPattern(data); if (!indexPattern) return
Loading...
; @@ -204,15 +205,7 @@ function useIndexPattern(data: DataPublicPluginStart) { return indexPattern; } -/** - * Setup syncing of state containers with url - * @param appStateContainer - * @param globalStateContainer - * @param data - * @param kbnUrlStateStorage - */ -function useStateSyncing( - appStateContainer: BaseStateContainer, +function useGlobalStateSyncing( globalStateContainer: BaseStateContainer, query: QueryStart, kbnUrlStateStorage: IKbnUrlStateStorage @@ -225,6 +218,48 @@ function useStateSyncing state && globalStateContainer.set(state), + }, + }); + + // merge initial state from global state container and current state in url + const initialGlobalState: GlobalState = { + ...globalStateContainer.get(), + ...kbnUrlStateStorage.get('_g'), + }; + // trigger state update. actually needed in case some data was in url + globalStateContainer.set(initialGlobalState); + + // set current url to whatever is in global state container + kbnUrlStateStorage.set('_g', initialGlobalState); + + // finally start syncing state containers with url + startSyncingGlobalStateWithUrl(); + + return () => { + stopSyncingQueryGlobalStateWithStateContainer(); + stopSyncingGlobalStateWithUrl(); + }; + }, [query, kbnUrlStateStorage, globalStateContainer]); +} + +function useAppStateSyncing( + appStateContainer: BaseStateContainer, + query: QueryStart, + kbnUrlStateStorage: IKbnUrlStateStorage +) { + // setup sync state utils + useEffect(() => { // sync app filters with app state container from data.query to state container const stopSyncingQueryAppStateWithStateContainer = connectToQueryAppState( query, @@ -242,20 +277,6 @@ function useStateSyncing state && globalStateContainer.set(state), - }, - }); - // merge initial state from app state container and current state in url const initialAppState: AppState = { ...appStateContainer.get(), @@ -264,29 +285,15 @@ function useStateSyncing('_g'), - }; - // trigger state update. actually needed in case some data was in url - globalStateContainer.set(initialGlobalState); - // set current url to whatever is in app state container kbnUrlStateStorage.set('_a', initialAppState); - // set current url to whatever is in global state container - kbnUrlStateStorage.set('_g', initialGlobalState); - // finally start syncing state containers with url startSyncingAppStateWithUrl(); - startSyncingGlobalStateWithUrl(); return () => { - stopSyncingQueryGlobalStateWithStateContainer(); stopSyncingQueryAppStateWithStateContainer(); stopSyncingAppStateWithUrl(); - stopSyncingGlobalStateWithUrl(); }; - }, [query, kbnUrlStateStorage, appStateContainer, globalStateContainer]); + }, [query, kbnUrlStateStorage, appStateContainer]); } From bc07de48522b8ac148bc3c1485e912ea42444ba7 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Feb 2020 15:18:38 +0100 Subject: [PATCH 07/29] merge --- .../core_plugins/kibana/public/dashboard/plugin.ts | 13 ++++++++----- .../public/query/state_sync/connect_to_app_state.ts | 2 +- .../query/state_sync/connect_to_global_state.ts | 2 +- .../query/state_sync/sync_global_state_with_url.ts | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 7ae1c723a3914..09d49a01e997f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -48,7 +48,8 @@ import { } from '../../../../../plugins/kibana_legacy/public'; import { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards'; import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { getQueryStateContainer } from '../../../../../plugins/data/public'; +import { connectToQueryGlobalState } from '../../../../../plugins/data/public'; +import { createStateContainer } from '../../../../../plugins/kibana_utils/common/state_containers/create_state_container'; export interface LegacyAngularInjectedDependencies { dashboardConfig: any; @@ -92,8 +93,10 @@ export class DashboardPlugin implements Plugin { npData, }: DashboardPluginSetupDependencies ) { - const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( - npData.query + const globalQueryStateContainer = createStateContainer({}); + const disconnectGlobalQueryStateContainer = connectToQueryGlobalState( + npData.query, + globalQueryStateContainer ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), @@ -104,12 +107,12 @@ export class DashboardPlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: querySyncStateContainer.state$, + stateUpdate$: globalQueryStateContainer.state$, }, ], }); this.stopUrlTracking = () => { - stopQuerySyncStateContainer(); + disconnectGlobalQueryStateContainer(); stopUrlTracker(); }; const app: App = { diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts index ba6262b187be8..44ce47a8ad113 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -35,7 +35,7 @@ export interface QueryAppState { * @param stateContainer */ export function connectToQueryAppState( - { filterManager }: QueryStart, + { filterManager }: Pick, appState: BaseStateContainer ) { // initial syncing diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts index 65b2e5e6a4aa3..6064a84daf766 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -38,7 +38,7 @@ export interface QueryGlobalState { * @param stateContainer */ export const connectToQueryGlobalState = ( - { timefilter: { timefilter }, filterManager }: QueryStart, + { timefilter: { timefilter }, filterManager }: Pick, globalState: BaseStateContainer ) => { // initial syncing diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts index 3bde0a966c5dc..63e1431a258ef 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts @@ -33,7 +33,7 @@ const GLOBAL_STATE_STORAGE_KEY = '_g'; * @param kbnUrlStateStorage - url storage to use */ export const syncGlobalQueryStateWithUrl = ( - query: QueryStart, + query: Pick, kbnUrlStateStorage: IKbnUrlStateStorage ) => { const { From 8be16b30a44a281025398d5962a5974cca214cf2 Mon Sep 17 00:00:00 2001 From: Liza K Date: Thu, 6 Feb 2020 17:05:53 +0200 Subject: [PATCH 08/29] top nav ts arg support timefilter initial state --- src/plugins/data/public/ui/index.ts | 2 +- .../public/ui/search_bar/create_search_bar.tsx | 4 ++++ .../data/public/ui/search_bar/index.tsx | 1 + .../public/ui/search_bar/lib/use_timefilter.ts | 18 +++++++++++++++--- .../data/public/ui/search_bar/search_bar.tsx | 10 +++++----- .../public/top_nav_menu/top_nav_menu.test.tsx | 14 +------------- .../public/top_nav_menu/top_nav_menu.tsx | 5 ++--- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 0755363c9b16b..5a1ad9957d7d7 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,7 +21,7 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { QueryStringInput } from './query_string_input/query_string_input'; -export { SearchBar, SearchBarProps } from './search_bar'; +export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; // @internal export { diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 71d76f4db49e2..c24c20bd08fb8 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -132,6 +132,10 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) filterManager: data.query.filterManager, }); const { timeRange, refreshInterval } = useTimefilter({ + dateRangeFrom: props.dateRangeFrom, + dateRangeTo: props.dateRangeTo, + refreshInterval: props.refreshInterval, + isRefreshPaused: props.isRefreshPaused, timefilter: data.query.timefilter.timefilter, }); diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index 4aa7f5fe2b040..fbc9f4a41ebbf 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -18,3 +18,4 @@ */ export { SearchBar, SearchBarProps } from './search_bar'; +export { StatefulSearchBarProps } from './create_search_bar'; diff --git a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts index 942902ebd7286..b56c717df4978 100644 --- a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts +++ b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts @@ -19,15 +19,27 @@ import { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, TimeRange, RefreshInterval } from 'src/plugins/data/public'; interface UseTimefilterProps { + dateRangeFrom?: string; + dateRangeTo?: string; + refreshInterval?: number; + isRefreshPaused?: boolean; timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; } export const useTimefilter = (props: UseTimefilterProps) => { - const [timeRange, setTimerange] = useState(props.timefilter.getTime()); - const [refreshInterval, setRefreshInterval] = useState(props.timefilter.getRefreshInterval()); + const initialTimeRange: TimeRange = { + from: props.dateRangeFrom || props.timefilter.getTime().from, + to: props.dateRangeTo || props.timefilter.getTime().to, + }; + const initialRefreshInterval: RefreshInterval = { + value: props.refreshInterval || props.timefilter.getRefreshInterval().value, + pause: props.isRefreshPaused || props.timefilter.getRefreshInterval().pause, + }; + const [timeRange, setTimerange] = useState(initialTimeRange); + const [refreshInterval, setRefreshInterval] = useState(initialRefreshInterval); useEffect(() => { const subscriptions = new Subscription(); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 2f0cdb322912b..8d2219bc5731f 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -47,13 +47,8 @@ interface SearchBarInjectedDeps { timeHistory: TimeHistoryContract; // Filter bar onFiltersUpdated?: (filters: esFilters.Filter[]) => void; - // Date picker - dateRangeFrom?: string; - dateRangeTo?: string; // Autorefresh onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; - isRefreshPaused?: boolean; - refreshInterval?: number; } export interface SearchBarOwnProps { @@ -69,6 +64,11 @@ export interface SearchBarOwnProps { showDatePicker?: boolean; showAutoRefreshOnly?: boolean; filters?: esFilters.Filter[]; + // Date picker + isRefreshPaused?: boolean; + refreshInterval?: number; + dateRangeFrom?: string; + dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; // Show when user has privileges to save diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 4e2ea44bf7642..8e0e8b3031132 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,13 +22,6 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -const mockTimeHistory = { - add: () => {}, - get: () => { - return []; - }, -}; - const dataShim = { ui: { SearchBar: () =>
, @@ -76,12 +69,7 @@ describe('TopNavMenu', () => { it('Should render search bar', () => { const component = shallowWithIntl( - + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 849a4b033399e..cf39c82eff3ce 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,10 +24,9 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps, DataPublicPluginStart } from '../../../data/public'; +import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; -export type TopNavMenuProps = Partial & { - appName: string; +export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; From 04fc4d5a70c6be7d2763d91034293019ae792377 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Feb 2020 19:02:16 +0100 Subject: [PATCH 09/29] wip --- .../public/with_data_services/components/app.tsx | 6 ++++++ .../data/public/query/timefilter/timefilter_service.mock.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index a5e6e912cd73e..4193d0c0d4fdf 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -54,6 +54,7 @@ import { syncState, } from '../../../../../src/plugins/kibana_utils/public'; import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; +import { Query } from '../../../../../src/plugins/data/common/query/types'; interface StateDemoAppDeps { notifications: CoreStart['notifications']; @@ -66,6 +67,7 @@ interface StateDemoAppDeps { interface AppState extends QueryAppState { name: string; + query?: Query; } const defaultAppState: AppState = { name: '', @@ -118,6 +120,10 @@ const App = ({ appName={PLUGIN_ID} showSearchBar={true} indexPatterns={[indexPattern]} + useDefaultBehaviors={true} + // TODO: would be cool to also get rid of this query syncing + onQuerySubmit={({ query }) => appStateContainer.set({ ...appState, query })} + query={appState.query} /> diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 2923cee60f898..d613731bfd783 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -42,6 +42,8 @@ const createSetupContractMock = () => { getBounds: jest.fn(), calculateBounds: jest.fn(), createFilter: jest.fn(), + getRefreshIntervalDefaults: jest.fn(), + getTimeDefaults: jest.fn(), }; const historyMock: jest.Mocked = { From ac55f336b620e07a9eaaca97ebbef4b7aa068dc8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Feb 2020 23:36:01 +0100 Subject: [PATCH 10/29] snapshots --- .../__snapshots__/query_string_input.test.tsx.snap | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2f2332bb06e3c..108e0499fbbd0 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -199,8 +199,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], @@ -840,8 +842,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], @@ -1463,8 +1467,10 @@ exports[`QueryStringInput Should pass the query language to the language switche "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], @@ -2101,8 +2107,10 @@ exports[`QueryStringInput Should pass the query language to the language switche "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], @@ -2724,8 +2732,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], @@ -3362,8 +3372,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction], From ce594617bfaa0cbe6352ff26fc4adceebcc75858 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 7 Feb 2020 00:03:21 +0100 Subject: [PATCH 11/29] use-callback --- .../public/with_data_services/components/app.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 4193d0c0d4fdf..4105e689dada8 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { History } from 'history'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; @@ -107,6 +107,13 @@ const App = ({ useGlobalStateSyncing(globalStateContainer, data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); + const onQuerySubmit = useCallback( + ({ query }) => { + appStateContainer.set({ ...appState, query }); + }, + [appStateContainer, appState] + ); + const indexPattern = useIndexPattern(data); if (!indexPattern) return
Loading...
; @@ -122,7 +129,7 @@ const App = ({ indexPatterns={[indexPattern]} useDefaultBehaviors={true} // TODO: would be cool to also get rid of this query syncing - onQuerySubmit={({ query }) => appStateContainer.set({ ...appState, query })} + onQuerySubmit={onQuerySubmit} query={appState.query} /> From f4fb6b85ca75285bb22ecf5c143c99379f8dfbcd Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 7 Feb 2020 13:28:53 +0100 Subject: [PATCH 12/29] wip --- .../with_data_services/components/app.tsx | 22 ++-- .../kibana/public/dashboard/plugin.ts | 10 +- .../data/public/query/query_service.ts | 105 +++++++++++++++++- .../query/state_sync/connect_to_app_state.ts | 20 +--- .../state_sync/connect_to_global_state.ts | 32 ++---- .../state_sync/sync_global_state_with_url.ts | 2 +- 6 files changed, 128 insertions(+), 63 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 4105e689dada8..f773dc5d5a12c 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { History } from 'history'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; @@ -54,7 +54,7 @@ import { syncState, } from '../../../../../src/plugins/kibana_utils/public'; import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; -import { Query } from '../../../../../src/plugins/data/common/query/types'; +// import { Query } from '../../../../../src/plugins/data/common/query/types'; interface StateDemoAppDeps { notifications: CoreStart['notifications']; @@ -67,7 +67,7 @@ interface StateDemoAppDeps { interface AppState extends QueryAppState { name: string; - query?: Query; + // query?: Query; } const defaultAppState: AppState = { name: '', @@ -107,12 +107,12 @@ const App = ({ useGlobalStateSyncing(globalStateContainer, data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); - const onQuerySubmit = useCallback( - ({ query }) => { - appStateContainer.set({ ...appState, query }); - }, - [appStateContainer, appState] - ); + // const onQuerySubmit = useCallback( + // ({ query }) => { + // appStateContainer.set({ ...appState, query }); + // }, + // [appStateContainer, appState] + // ); const indexPattern = useIndexPattern(data); if (!indexPattern) return
Loading...
; @@ -129,8 +129,8 @@ const App = ({ indexPatterns={[indexPattern]} useDefaultBehaviors={true} // TODO: would be cool to also get rid of this query syncing - onQuerySubmit={onQuerySubmit} - query={appState.query} + // onQuerySubmit={onQuerySubmit} + // query={appState.query} /> diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 09d49a01e997f..5006b36061adc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -48,8 +48,6 @@ import { } from '../../../../../plugins/kibana_legacy/public'; import { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards'; import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { connectToQueryGlobalState } from '../../../../../plugins/data/public'; -import { createStateContainer } from '../../../../../plugins/kibana_utils/common/state_containers/create_state_container'; export interface LegacyAngularInjectedDependencies { dashboardConfig: any; @@ -93,11 +91,6 @@ export class DashboardPlugin implements Plugin { npData, }: DashboardPluginSetupDependencies ) { - const globalQueryStateContainer = createStateContainer({}); - const disconnectGlobalQueryStateContainer = connectToQueryGlobalState( - npData.query, - globalQueryStateContainer - ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/dashboards', @@ -107,12 +100,11 @@ export class DashboardPlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: globalQueryStateContainer.state$, + stateUpdate$: npData.query.global$, }, ], }); this.stopUrlTracking = () => { - disconnectGlobalQueryStateContainer(); stopUrlTracker(); }; const app: App = { diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index ebef8b8d45050..b87e43d6dd89e 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -17,11 +17,95 @@ * under the License. */ +import { Observable, Subscription } from 'rxjs'; +import { filter, map, share } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { FilterManager } from './filter_manager'; +import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from './filter_manager'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; +import { createStateContainer } from '../../../kibana_utils/common/state_containers/create_state_container'; +import { QueryAppState, QueryGlobalState } from './state_sync'; + +function createGlobalQueryObservable({ + timefilter: { timefilter }, + filterManager, +}: { + timefilter: TimefilterSetup; + filterManager: FilterManager; +}): Observable { + return new Observable(subscriber => { + const state = createStateContainer({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + filters: filterManager.getGlobalFilters(), + }); + + const subs: Subscription[] = [ + timefilter.getTimeUpdate$().subscribe(() => { + state.set({ ...state.get(), time: timefilter.getTime() }); + }), + timefilter.getRefreshIntervalUpdate$().subscribe(() => { + state.set({ ...state.get(), refreshInterval: timefilter.getRefreshInterval() }); + }), + filterManager + .getUpdates$() + .pipe( + // we need to track only global filters here + map(() => filterManager.getGlobalFilters()), + // continue only if global filters changed + // and ignore app state filters + filter( + newGlobalFilters => + !compareFilters(newGlobalFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) + ) + ) + .subscribe(newGlobalFilters => { + state.set({ ...state.get(), filters: newGlobalFilters }); + }), + state.state$.subscribe(subscriber), + ]; + return () => { + subs.forEach(s => s.unsubscribe()); + }; + }); +} + +function createAppQueryObservable({ + timefilter: { timefilter }, + filterManager, +}: { + timefilter: TimefilterSetup; + filterManager: FilterManager; +}): Observable { + return new Observable(subscriber => { + const state = createStateContainer({ + filters: filterManager.getAppFilters(), + }); + + const subs: Subscription[] = [ + filterManager + .getUpdates$() + .pipe( + // we need to track only app filters here + map(() => filterManager.getAppFilters()), + // continue only if app filters changed + // and ignore global state filters + filter( + newAppFilters => + !compareFilters(newAppFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) + ) + ) + .subscribe(newAppFilters => { + state.set({ ...state.get(), filters: newAppFilters }); + }), + state.state$.subscribe(subscriber), + ]; + return () => { + subs.forEach(s => s.unsubscribe()); + }; + }); +} /** * Query Service @@ -36,6 +120,9 @@ export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; + app$!: Observable; + global$!: Observable; + public setup({ uiSettings, storage }: QueryServiceDependencies) { this.filterManager = new FilterManager(uiSettings); @@ -45,9 +132,21 @@ export class QueryService { storage, }); + this.global$ = createGlobalQueryObservable({ + filterManager: this.filterManager, + timefilter: this.timefilter, + }).pipe(share()); + + this.app$ = createAppQueryObservable({ + filterManager: this.filterManager, + timefilter: this.timefilter, + }).pipe(share()); + return { filterManager: this.filterManager, timefilter: this.timefilter, + global$: this.global$, + app$: this.app$, }; } @@ -55,12 +154,14 @@ export class QueryService { return { filterManager: this.filterManager, timefilter: this.timefilter, + global$: this.global$, + app$: this.app$, savedQueries: createSavedQueryService(savedObjects.client), }; } public stop() { - // nothing to do here yet + // nothing here yet } } diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts index 44ce47a8ad113..d2abce041e8e2 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { filter, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { esFilters } from '../../../common'; import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; @@ -35,7 +35,7 @@ export interface QueryAppState { * @param stateContainer */ export function connectToQueryAppState( - { filterManager }: Pick, + { filterManager, app$ }: Pick, appState: BaseStateContainer ) { // initial syncing @@ -47,19 +47,9 @@ export function connectToQueryAppState( // subscribe to updates const subs = [ - filterManager - .getUpdates$() - .pipe( - map(() => filterManager.getAppFilters()), - filter( - // continue only if app state filters updated - appFilters => - !compareFilters(appFilters, appState.get().filters || [], COMPARE_ALL_OPTIONS) - ) - ) - .subscribe(appFilters => { - appState.set({ ...appState.get(), filters: appFilters } as S); - }), + app$.subscribe(appQueryState => { + appState.set({ ...appState.get(), ...appQueryState } as S); + }), // if appFilters in dashboardStateManager changed (e.g browser history update), // sync it to filterManager diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts index 6064a84daf766..6e4111c5bae10 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -19,7 +19,6 @@ import { Subscription } from 'rxjs'; import _ from 'lodash'; -import { filter, map } from 'rxjs/operators'; import { BaseStateContainer } from '../../../../kibana_utils/public'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { esFilters, RefreshInterval, TimeRange } from '../../../common'; @@ -38,7 +37,11 @@ export interface QueryGlobalState { * @param stateContainer */ export const connectToQueryGlobalState = ( - { timefilter: { timefilter }, filterManager }: Pick, + { + timefilter: { timefilter }, + filterManager, + global$, + }: Pick, globalState: BaseStateContainer ) => { // initial syncing @@ -54,30 +57,9 @@ export const connectToQueryGlobalState = ( } as S); const subs: Subscription[] = [ - timefilter.getTimeUpdate$().subscribe(() => { - globalState.set({ ...globalState.get(), time: timefilter.getTime() } as S); + global$.subscribe(newGlobalQueryState => { + globalState.set({ ...globalState.get(), ...newGlobalQueryState } as S); }), - timefilter.getRefreshIntervalUpdate$().subscribe(() => { - globalState.set({ - ...globalState.get(), - refreshInterval: timefilter.getRefreshInterval(), - } as S); - }), - filterManager - .getUpdates$() - .pipe( - // we need to track only global filters here - map(() => filterManager.getGlobalFilters()), - // continue only if global filters changed - // and ignore app state filters - filter( - newGlobalFilters => - !compareFilters(newGlobalFilters, globalState.get().filters || [], COMPARE_ALL_OPTIONS) - ) - ) - .subscribe(newGlobalFilters => { - globalState.set({ ...globalState.get(), filters: newGlobalFilters } as S); - }), globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { // cloneDeep is required because services are mutating passed objects // and state in state container is frozen diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts index 63e1431a258ef..9258ecb64d606 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts @@ -33,7 +33,7 @@ const GLOBAL_STATE_STORAGE_KEY = '_g'; * @param kbnUrlStateStorage - url storage to use */ export const syncGlobalQueryStateWithUrl = ( - query: Pick, + query: Pick, kbnUrlStateStorage: IKbnUrlStateStorage ) => { const { From e6565789aa9942e9ce68d460f11907baa51afece Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 7 Feb 2020 14:02:48 +0100 Subject: [PATCH 13/29] wip --- src/plugins/data/public/query/mocks.ts | 9 +- .../data/public/query/query_service.ts | 89 ++----------------- .../data/public/query/state_sync/README.md | 3 + .../query/state_sync/connect_to_app_state.ts | 21 ++--- .../state_sync/connect_to_global_state.ts | 21 ++--- .../state_sync/create_app_query_observable.ts | 61 +++++++++++++ .../create_global_query_observable.ts | 69 ++++++++++++++ .../data/public/query/state_sync/index.ts | 5 +- .../sync_global_state_with_url.test.ts | 2 +- .../state_sync/sync_global_state_with_url.ts | 13 +-- .../data/public/query/state_sync/types.ts | 36 ++++++++ 11 files changed, 207 insertions(+), 122 deletions(-) create mode 100644 src/plugins/data/public/query/state_sync/README.md create mode 100644 src/plugins/data/public/query/state_sync/create_app_query_observable.ts create mode 100644 src/plugins/data/public/query/state_sync/create_global_query_observable.ts create mode 100644 src/plugins/data/public/query/state_sync/types.ts diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 2710dadaa23a3..e6b079a0f4632 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -17,7 +17,8 @@ * under the License. */ -import { QueryService, QuerySetup } from '.'; +import { Observable } from 'rxjs'; +import { QueryService, QuerySetup, QueryStart } from '.'; import { timefilterServiceMock } from './timefilter/timefilter_service.mock'; type QueryServiceClientContract = PublicMethodsOf; @@ -26,16 +27,20 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { filterManager: jest.fn() as any, timefilter: timefilterServiceMock.createSetupContract(), + global$: new Observable(), + app$: new Observable(), }; return setupContract; }; const createStartContractMock = () => { - const startContract = { + const startContract: jest.Mocked = { filterManager: jest.fn() as any, timefilter: timefilterServiceMock.createStartContract(), savedQueries: jest.fn() as any, + global$: new Observable(), + app$: new Observable(), }; return startContract; diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index b87e43d6dd89e..bf04d61a75801 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -17,95 +17,16 @@ * under the License. */ -import { Observable, Subscription } from 'rxjs'; -import { filter, map, share } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { share } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from './filter_manager'; +import { FilterManager } from './filter_manager'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; -import { createStateContainer } from '../../../kibana_utils/common/state_containers/create_state_container'; import { QueryAppState, QueryGlobalState } from './state_sync'; - -function createGlobalQueryObservable({ - timefilter: { timefilter }, - filterManager, -}: { - timefilter: TimefilterSetup; - filterManager: FilterManager; -}): Observable { - return new Observable(subscriber => { - const state = createStateContainer({ - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - filters: filterManager.getGlobalFilters(), - }); - - const subs: Subscription[] = [ - timefilter.getTimeUpdate$().subscribe(() => { - state.set({ ...state.get(), time: timefilter.getTime() }); - }), - timefilter.getRefreshIntervalUpdate$().subscribe(() => { - state.set({ ...state.get(), refreshInterval: timefilter.getRefreshInterval() }); - }), - filterManager - .getUpdates$() - .pipe( - // we need to track only global filters here - map(() => filterManager.getGlobalFilters()), - // continue only if global filters changed - // and ignore app state filters - filter( - newGlobalFilters => - !compareFilters(newGlobalFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) - ) - ) - .subscribe(newGlobalFilters => { - state.set({ ...state.get(), filters: newGlobalFilters }); - }), - state.state$.subscribe(subscriber), - ]; - return () => { - subs.forEach(s => s.unsubscribe()); - }; - }); -} - -function createAppQueryObservable({ - timefilter: { timefilter }, - filterManager, -}: { - timefilter: TimefilterSetup; - filterManager: FilterManager; -}): Observable { - return new Observable(subscriber => { - const state = createStateContainer({ - filters: filterManager.getAppFilters(), - }); - - const subs: Subscription[] = [ - filterManager - .getUpdates$() - .pipe( - // we need to track only app filters here - map(() => filterManager.getAppFilters()), - // continue only if app filters changed - // and ignore global state filters - filter( - newAppFilters => - !compareFilters(newAppFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) - ) - ) - .subscribe(newAppFilters => { - state.set({ ...state.get(), filters: newAppFilters }); - }), - state.state$.subscribe(subscriber), - ]; - return () => { - subs.forEach(s => s.unsubscribe()); - }; - }); -} +import { createGlobalQueryObservable } from './state_sync/create_global_query_observable'; +import { createAppQueryObservable } from './state_sync/create_app_query_observable'; /** * Query Service diff --git a/src/plugins/data/public/query/state_sync/README.md b/src/plugins/data/public/query/state_sync/README.md new file mode 100644 index 0000000000000..6b9b158100573 --- /dev/null +++ b/src/plugins/data/public/query/state_sync/README.md @@ -0,0 +1,3 @@ +# Query state syncing utilities + +Set of helpers to connect data services to state containers and state syncing utilities diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts index d2abce041e8e2..2ccce6eaa0719 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -20,22 +20,17 @@ import _ from 'lodash'; import { map } from 'rxjs/operators'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; -import { esFilters } from '../../../common'; import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; -import { QueryStart } from '../query_service'; - -export interface QueryAppState { - filters?: esFilters.Filter[]; -} +import { QuerySetup, QueryStart } from '../query_service'; +import { QueryAppState } from './types'; /** - * Helper utility to sync app state data from query services: app filters (not pinned) - * with state container - * @param QueryStart - * @param stateContainer + * Helper to setup two-way syncing of app scoped data and a state container + * @param QueryService: either setup or start + * @param stateContainer to use for syncing */ export function connectToQueryAppState( - { filterManager, app$ }: Pick, + { filterManager, app$ }: Pick, appState: BaseStateContainer ) { // initial syncing @@ -43,12 +38,12 @@ export function connectToQueryAppState( // filterManager takes precedence, this seems like a good default, // and apps could anyway set their own value after initialisation, // but maybe maybe this should be a configurable option? - appState.set({ ...appState.get(), filters: filterManager.getAppFilters() } as S); + appState.set({ ...appState.get(), filters: filterManager.getAppFilters() }); // subscribe to updates const subs = [ app$.subscribe(appQueryState => { - appState.set({ ...appState.get(), ...appQueryState } as S); + appState.set({ ...appState.get(), ...appQueryState }); }), // if appFilters in dashboardStateManager changed (e.g browser history update), diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts index 6e4111c5bae10..91365eda1ea90 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -21,27 +21,20 @@ import { Subscription } from 'rxjs'; import _ from 'lodash'; import { BaseStateContainer } from '../../../../kibana_utils/public'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; -import { esFilters, RefreshInterval, TimeRange } from '../../../common'; -import { QueryStart } from '../query_service'; - -export interface QueryGlobalState { - time?: TimeRange; - refreshInterval?: RefreshInterval; - filters?: esFilters.Filter[]; -} +import { QuerySetup, QueryStart } from '../query_service'; +import { QueryGlobalState } from './types'; /** - * Helper utility to sync global data from query services: time, refreshInterval, global (pinned) filters - * with state container - * @param QueryStart - * @param stateContainer + * Helper to setup two-way syncing of global data and a state container + * @param QueryService: either setup or start + * @param stateContainer to use for syncing */ export const connectToQueryGlobalState = ( { timefilter: { timefilter }, filterManager, global$, - }: Pick, + }: Pick, globalState: BaseStateContainer ) => { // initial syncing @@ -58,7 +51,7 @@ export const connectToQueryGlobalState = ( const subs: Subscription[] = [ global$.subscribe(newGlobalQueryState => { - globalState.set({ ...globalState.get(), ...newGlobalQueryState } as S); + globalState.set({ ...globalState.get(), ...newGlobalQueryState }); }), globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { // cloneDeep is required because services are mutating passed objects diff --git a/src/plugins/data/public/query/state_sync/create_app_query_observable.ts b/src/plugins/data/public/query/state_sync/create_app_query_observable.ts new file mode 100644 index 0000000000000..770f894384202 --- /dev/null +++ b/src/plugins/data/public/query/state_sync/create_app_query_observable.ts @@ -0,0 +1,61 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { TimefilterSetup } from '../timefilter'; +import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; +import { QueryAppState } from './index'; +import { createStateContainer } from '../../../../kibana_utils/common/state_containers'; + +export function createAppQueryObservable({ + timefilter: { timefilter }, + filterManager, +}: { + timefilter: TimefilterSetup; + filterManager: FilterManager; +}): Observable { + return new Observable(subscriber => { + const state = createStateContainer({ + filters: filterManager.getAppFilters(), + }); + + const subs: Subscription[] = [ + filterManager + .getUpdates$() + .pipe( + // we need to track only app filters here + map(() => filterManager.getAppFilters()), + // continue only if app filters changed + // and ignore global state filters + filter( + newAppFilters => + !compareFilters(newAppFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) + ) + ) + .subscribe(newAppFilters => { + state.set({ ...state.get(), filters: newAppFilters }); + }), + state.state$.subscribe(subscriber), + ]; + return () => { + subs.forEach(s => s.unsubscribe()); + }; + }); +} diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts new file mode 100644 index 0000000000000..6aabc6fdeb09b --- /dev/null +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -0,0 +1,69 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { TimefilterSetup } from '../timefilter'; +import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; +import { QueryGlobalState } from './index'; +import { createStateContainer } from '../../../../kibana_utils/public'; + +export function createGlobalQueryObservable({ + timefilter: { timefilter }, + filterManager, +}: { + timefilter: TimefilterSetup; + filterManager: FilterManager; +}): Observable { + return new Observable(subscriber => { + const state = createStateContainer({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + filters: filterManager.getGlobalFilters(), + }); + + const subs: Subscription[] = [ + timefilter.getTimeUpdate$().subscribe(() => { + state.set({ ...state.get(), time: timefilter.getTime() }); + }), + timefilter.getRefreshIntervalUpdate$().subscribe(() => { + state.set({ ...state.get(), refreshInterval: timefilter.getRefreshInterval() }); + }), + filterManager + .getUpdates$() + .pipe( + // we need to track only global filters here + map(() => filterManager.getGlobalFilters()), + // continue only if global filters changed + // and ignore app state filters + filter( + newGlobalFilters => + !compareFilters(newGlobalFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) + ) + ) + .subscribe(newGlobalFilters => { + state.set({ ...state.get(), filters: newGlobalFilters }); + }), + state.state$.subscribe(subscriber), + ]; + return () => { + subs.forEach(s => s.unsubscribe()); + }; + }); +} diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index fe18ffec2bdd2..dfdf4d6d0f210 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -17,6 +17,7 @@ * under the License. */ -export { connectToQueryGlobalState, QueryGlobalState } from './connect_to_global_state'; -export { connectToQueryAppState, QueryAppState } from './connect_to_app_state'; +export { connectToQueryGlobalState } from './connect_to_global_state'; +export { connectToQueryAppState } from './connect_to_app_state'; export { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; +export { QueryAppState, QueryGlobalState } from './types'; diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts index 3bafd49b0b766..05df85211ddf4 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts @@ -31,8 +31,8 @@ import { import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { TimefilterContract } from '../timefilter'; -import { QueryGlobalState } from './connect_to_global_state'; import { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; +import { QueryGlobalState } from './types'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts index 9258ecb64d606..4096cbdfce1c9 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts @@ -22,18 +22,19 @@ import { syncState, createStateContainer, } from '../../../../kibana_utils/public'; -import { QueryStart } from '../query_service'; -import { connectToQueryGlobalState, QueryGlobalState } from './connect_to_global_state'; +import { QuerySetup, QueryStart } from '../query_service'; +import { connectToQueryGlobalState } from './connect_to_global_state'; +import { QueryGlobalState } from './types'; const GLOBAL_STATE_STORAGE_KEY = '_g'; /** - * Helper utility to sync global data from query services with url ('_g' query param) - * @param QueryStart - * @param kbnUrlStateStorage - url storage to use + * Helper to setup syncing of global data with the URL + * @param QueryService: either setup or start + * @param kbnUrlStateStorage to use for syncing */ export const syncGlobalQueryStateWithUrl = ( - query: Pick, + query: Pick, kbnUrlStateStorage: IKbnUrlStateStorage ) => { const { diff --git a/src/plugins/data/public/query/state_sync/types.ts b/src/plugins/data/public/query/state_sync/types.ts new file mode 100644 index 0000000000000..6b1b0d5066d58 --- /dev/null +++ b/src/plugins/data/public/query/state_sync/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { esFilters, RefreshInterval, TimeRange } from '../../../common'; + +/** + * State from data services that meant to be preserved between apps + */ +export interface QueryGlobalState { + time?: TimeRange; + refreshInterval?: RefreshInterval; + filters?: esFilters.Filter[]; // pinned filters only +} + +/** + * State from data services that should be scope to a single app + */ +export interface QueryAppState { + filters?: esFilters.Filter[]; // not pinned filters +} From 962f84f26ded02a248c45694da0845f414b6781c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 7 Feb 2020 15:02:46 +0100 Subject: [PATCH 14/29] wip --- .../query/state_sync/connect_to_app_state.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts index 2ccce6eaa0719..8690ee506c1a3 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { map } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; import { QuerySetup, QueryStart } from '../query_service'; @@ -33,26 +33,35 @@ export function connectToQueryAppState( { filterManager, app$ }: Pick, appState: BaseStateContainer ) { + function shouldSync() { + const stateContainerFilters = appState.get().filters; + if (!stateContainerFilters) return true; + const filterManagerFilters = filterManager.getAppFilters(); + const areAppFiltersEqual = compareFilters( + stateContainerFilters, + filterManagerFilters, + COMPARE_ALL_OPTIONS + ); + if (areAppFiltersEqual) return false; + + return true; + } + // initial syncing // TODO: // filterManager takes precedence, this seems like a good default, // and apps could anyway set their own value after initialisation, // but maybe maybe this should be a configurable option? - appState.set({ ...appState.get(), filters: filterManager.getAppFilters() }); + if (shouldSync()) { + appState.set({ ...appState.get(), filters: filterManager.getAppFilters() }); + } - // subscribe to updates const subs = [ - app$.subscribe(appQueryState => { + app$.pipe(filter(shouldSync)).subscribe(appQueryState => { appState.set({ ...appState.get(), ...appQueryState }); }), - - // if appFilters in dashboardStateManager changed (e.g browser history update), - // sync it to filterManager - appState.state$.pipe(map(state => state.filters)).subscribe(appFilters => { - appFilters = appFilters || []; - if (!compareFilters(appFilters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS)) { - filterManager.setAppFilters(_.cloneDeep(appFilters)); - } + appState.state$.pipe(filter(shouldSync)).subscribe(appFilters => { + filterManager.setAppFilters(_.cloneDeep(appFilters.filters || [])); }), ]; From 29516bd97a1b18e6699b8f5f825263bb00ca5d63 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Sun, 16 Feb 2020 12:44:27 +0100 Subject: [PATCH 15/29] wip --- .../public/dashboard/np_ready/legacy_app.js | 53 +++--- .../kibana/public/dashboard/plugin.ts | 9 +- src/plugins/data/public/index.ts | 9 +- .../state_sync/connect_to_app_state.test.ts | 5 +- .../query/state_sync/connect_to_app_state.ts | 3 +- .../connect_to_global_state.test.ts | 177 ++++++++++++------ .../state_sync/connect_to_global_state.ts | 41 +++- .../data/public/query/state_sync/types.ts | 6 +- 8 files changed, 188 insertions(+), 115 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 9161d44e02905..1322821ba2ed6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -137,36 +137,31 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then( - () => { - const savedObjectsClient = deps.savedObjectsClient; - const title = $route.current.params.title; - if (title) { - return savedObjectsClient - .find({ - search: `"${title}"`, - search_fields: 'title', - type: 'dashboard', - }) - .then(results => { - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = results.savedObjects.filter( - dashboard => - dashboard.attributes.title.toLowerCase() === title.toLowerCase() - ); - if (matchingDashboards.length === 1) { - history.replace(createDashboardEditUrl(matchingDashboards[0].id)); - } else { - history.replace( - `${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"` - ); - $route.reload(); - } - return new Promise(() => {}); - }); - } + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { + const savedObjectsClient = deps.savedObjectsClient; + const title = $route.current.params.title; + if (title) { + return savedObjectsClient + .find({ + search: `"${title}"`, + search_fields: 'title', + type: 'dashboard', + }) + .then(results => { + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + history.replace(createDashboardEditUrl(matchingDashboards[0].id)); + } else { + history.replace(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); + $route.reload(); + } + return new Promise(() => {}); + }); } - ); + }); }, }, }) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index d235c35390bc6..2ee8c3b2b37ea 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -76,14 +76,7 @@ export class DashboardPlugin implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( - core: CoreSetup, - { - home, - kibanaLegacy, - data, - }: DashboardPluginSetupDependencies - ) { + public setup(core: CoreSetup, { home, kibanaLegacy, data }: DashboardPluginSetupDependencies) { const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/dashboards', diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 8704ca08ae905..b470788fce9e9 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -288,11 +288,14 @@ export { Filter, Query, RefreshInterval, TimeRange } from '../common'; export { createSavedQueryService, - syncAppFilters, - syncQuery, + connectToQueryGlobalState, + connectToQueryAppState, + syncGlobalQueryStateWithUrl, + QueryGlobalState, + QueryAppState, + QueryStart, getTime, getQueryLog, - getQueryStateContainer, FilterManager, SavedQuery, SavedQueryService, diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts index 03fc65e5e58bb..1b0deb26ad991 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts @@ -26,6 +26,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; +import { QueryAppState } from './types'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -46,7 +47,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { describe('connect_to_app_state', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; - let appState: BaseStateContainer<{ filters: Filter[] }>; + let appState: BaseStateContainer; let appStateSub: Subscription; let appStateChangeTriggered = jest.fn(); let filterManagerChangeSub: Subscription; @@ -66,7 +67,7 @@ describe('connect_to_app_state', () => { queryServiceStart = queryService.start(startMock.savedObjects); filterManager = queryServiceStart.filterManager; - appState = createStateContainer({ filters: [] as esFilters.Filter[] }); + appState = createStateContainer({}); appStateChangeTriggered = jest.fn(); appStateSub = appState.state$.subscribe(appStateChangeTriggered); diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts index 8690ee506c1a3..2bc76fd0d3296 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts @@ -42,9 +42,8 @@ export function connectToQueryAppState( filterManagerFilters, COMPARE_ALL_OPTIONS ); - if (areAppFiltersEqual) return false; - return true; + return !areAppFiltersEqual; } // initial syncing diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts index fd9f431c77d9b..3088cf8717f36 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts @@ -21,11 +21,13 @@ import { Subscription } from 'rxjs'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; import { Filter, FilterStateStore } from '../../../common'; -import { connectToQueryAppState } from './connect_to_app_state'; import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; +import { connectToQueryGlobalState } from './connect_to_global_state'; +import { TimefilterContract } from '../timefilter'; +import { QueryGlobalState } from './types'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -43,12 +45,13 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { } }); -describe('connect_to_app_state', () => { +describe('connect_to_global_state', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; - let appState: BaseStateContainer<{ filters: Filter[] }>; - let appStateSub: Subscription; - let appStateChangeTriggered = jest.fn(); + let timeFilter: TimefilterContract; + let globalState: BaseStateContainer; + let globalStateSub: Subscription; + let globalStateChangeTriggered = jest.fn(); let filterManagerChangeSub: Subscription; let filterManagerChangeTriggered = jest.fn(); @@ -65,10 +68,11 @@ describe('connect_to_app_state', () => { }); queryServiceStart = queryService.start(startMock.savedObjects); filterManager = queryServiceStart.filterManager; + timeFilter = queryServiceStart.timefilter.timefilter; - appState = createStateContainer({ filters: [] as Filter[] }); - appStateChangeTriggered = jest.fn(); - appStateSub = appState.state$.subscribe(appStateChangeTriggered); + globalState = createStateContainer({}); + globalStateChangeTriggered = jest.fn(); + globalStateSub = globalState.state$.subscribe(globalStateChangeTriggered); filterManagerChangeTriggered = jest.fn(); filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered); @@ -79,120 +83,171 @@ describe('connect_to_app_state', () => { aF2 = getFilter(FilterStateStore.APP_STATE, false, false, 'key4', 'value4'); }); afterEach(() => { - appStateSub.unsubscribe(); + globalStateSub.unsubscribe(); filterManagerChangeSub.unsubscribe(); }); - describe('sync from filterManager to app state', () => { - test('should sync app filters to app state when new app filters set to filterManager', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); + test('state is initialized with state from query service', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + + expect(globalState.get()).toEqual({ + filters: filterManager.getGlobalFilters(), + refreshInterval: timeFilter.getRefreshInterval(), + time: timeFilter.getTime(), + }); + + stop(); + }); + + test('when time range changes, state container contains updated time range', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + timeFilter.setTime({ from: 'now-30m', to: 'now' }); + expect(globalState.get().time).toEqual({ + from: 'now-30m', + to: 'now', + }); + stop(); + }); + + test('when refresh interval changes, state container contains updated refresh interval', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + timeFilter.setRefreshInterval({ pause: true, value: 100 }); + expect(globalState.get().refreshInterval).toEqual({ + pause: true, + value: 100, + }); + stop(); + }); + + test('state changes should propagate to services', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalStateChangeTriggered.mockClear(); + globalState.set({ + ...globalState.get(), + filters: [gF1, gF2], + refreshInterval: { pause: true, value: 100 }, + time: { from: 'now-30m', to: 'now' }, + }); + + expect(globalStateChangeTriggered).toBeCalledTimes(1); + + expect(filterManager.getGlobalFilters()).toHaveLength(2); + expect(timeFilter.getRefreshInterval()).toEqual({ pause: true, value: 100 }); + expect(timeFilter.getTime()).toEqual({ from: 'now-30m', to: 'now' }); + stop(); + }); + + describe('sync from filterManager to global state', () => { + test('should sync global filters to global state when new global filters set to filterManager', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); filterManager.setFilters([gF1, aF1]); - expect(appState.get().filters).toHaveLength(1); + expect(globalState.get().filters).toHaveLength(1); stop(); }); - test('should not sync global filters to app state ', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); + test('should not sync app filters to global state ', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); - filterManager.setFilters([gF1, gF2]); + filterManager.setFilters([aF1, aF2]); - expect(appState.get().filters).toHaveLength(0); + expect(globalState.get().filters).toHaveLength(0); stop(); }); - test("should not trigger changes when app filters didn't change", () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); + test("should not trigger changes when global filters didn't change", () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalStateChangeTriggered.mockClear(); filterManager.setFilters([gF1, aF1]); - filterManager.setFilters([gF2, aF1]); + filterManager.setFilters([gF1, aF2]); - expect(appStateChangeTriggered).toBeCalledTimes(1); - expect(appState.get().filters).toHaveLength(1); + expect(globalStateChangeTriggered).toBeCalledTimes(1); + expect(globalState.get().filters).toHaveLength(1); stop(); }); - test('should trigger changes when app filters change', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); + test('should trigger changes when global filters change', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalStateChangeTriggered.mockClear(); filterManager.setFilters([gF1, aF1]); - filterManager.setFilters([gF1, aF2]); + filterManager.setFilters([gF2, aF1]); - expect(appStateChangeTriggered).toBeCalledTimes(2); - expect(appState.get().filters).toHaveLength(1); + expect(globalStateChangeTriggered).toBeCalledTimes(2); + expect(globalState.get().filters).toHaveLength(1); stop(); }); - test('resetting filters should sync to app state', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); + test('resetting filters should sync to global state', () => { + const stop = connectToQueryGlobalState(queryServiceStart, globalState); filterManager.setFilters([gF1, aF1]); - expect(appState.get().filters).toHaveLength(1); + expect(globalState.get().filters).toHaveLength(1); filterManager.removeAll(); - expect(appState.get().filters).toHaveLength(0); + expect(globalState.get().filters).toHaveLength(0); stop(); }); test("shouldn't sync filters when syncing is stopped", () => { - const stop = connectToQueryAppState(queryServiceStart, appState); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); filterManager.setFilters([gF1, aF1]); - expect(appState.get().filters).toHaveLength(1); + expect(globalState.get().filters).toHaveLength(1); stop(); filterManager.removeAll(); - expect(appState.get().filters).toHaveLength(1); + expect(globalState.get().filters).toHaveLength(1); }); test('should pick up initial state from filterManager', () => { - appState.set({ filters: [aF1] }); - filterManager.setFilters([gF1]); + globalState.set({ filters: [gF1] }); + filterManager.setFilters([aF1]); - appStateChangeTriggered.mockClear(); - const stop = connectToQueryAppState(queryServiceStart, appState); - expect(appStateChangeTriggered).toBeCalledTimes(1); - expect(appState.get().filters).toHaveLength(0); + globalStateChangeTriggered.mockClear(); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + expect(globalStateChangeTriggered).toBeCalledTimes(1); + expect(globalState.get().filters).toHaveLength(0); stop(); }); }); - describe('sync from app state to filterManager', () => { - test('changes to app state should be synced to app filters', () => { - filterManager.setFilters([gF1]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); + describe('sync from global state to filterManager', () => { + test('changes to global state should be synced to global filters', () => { + filterManager.setFilters([aF1]); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalStateChangeTriggered.mockClear(); - appState.set({ filters: [aF1] }); + globalState.set({ ...globalState.get(), filters: [gF1] }); expect(filterManager.getFilters()).toHaveLength(2); expect(filterManager.getAppFilters()).toHaveLength(1); expect(filterManager.getGlobalFilters()).toHaveLength(1); - expect(appStateChangeTriggered).toBeCalledTimes(1); + expect(globalStateChangeTriggered).toBeCalledTimes(1); stop(); }); - test('global filters should remain untouched', () => { + test('app filters should remain untouched', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalStateChangeTriggered.mockClear(); - appState.set({ filters: [] }); + globalState.set({ ...globalState.get(), filters: [] }); expect(filterManager.getFilters()).toHaveLength(2); - expect(filterManager.getGlobalFilters()).toHaveLength(2); - expect(appStateChangeTriggered).toBeCalledTimes(1); + expect(filterManager.getAppFilters()).toHaveLength(2); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(globalStateChangeTriggered).toBeCalledTimes(1); stop(); }); @@ -200,9 +255,9 @@ describe('connect_to_app_state', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); filterManagerChangeTriggered.mockClear(); - appState.set({ filters: [aF1, aF2] }); - const stop = connectToQueryAppState(queryServiceStart, appState); - appState.set({ filters: [aF1, aF2] }); + globalState.set({ ...globalState.get(), filters: [gF1, gF2] }); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalState.set({ ...globalState.get(), filters: [gF1, gF2] }); expect(filterManagerChangeTriggered).toBeCalledTimes(0); stop(); @@ -210,11 +265,11 @@ describe('connect_to_app_state', () => { test('stop() should stop syncing', () => { filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appState.set({ filters: [] }); + const stop = connectToQueryGlobalState(queryServiceStart, globalState); + globalState.set({ ...globalState.get(), filters: [] }); expect(filterManager.getFilters()).toHaveLength(2); stop(); - appState.set({ filters: [aF1] }); + globalState.set({ ...globalState.get(), filters: [gF1] }); expect(filterManager.getFilters()).toHaveLength(2); }); }); diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts index 91365eda1ea90..2bd2d1312a5a9 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts @@ -18,6 +18,7 @@ */ import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import _ from 'lodash'; import { BaseStateContainer } from '../../../../kibana_utils/public'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; @@ -37,23 +38,47 @@ export const connectToQueryGlobalState = ( }: Pick, globalState: BaseStateContainer ) => { + let updateInProgress = false; + function shouldSync() { + if (updateInProgress) return false; + const { filters, refreshInterval, time } = globalState.get(); + if (!filters) return true; + const areGlobalFiltersEqual = compareFilters( + filters, + filterManager.getGlobalFilters(), + COMPARE_ALL_OPTIONS + ); + if (!areGlobalFiltersEqual) return true; + + const areRefreshIntervalsEqual = _.isEqual(refreshInterval, timefilter.getRefreshInterval()); + if (!areRefreshIntervalsEqual) return true; + + const areTimesEqual = _.isEqual(time, timefilter.getTime()); + if (!areTimesEqual) return true; + + return false; + } + // initial syncing // TODO: // data services take precedence, this seems like a good default, // and apps could anyway set their own value after initialisation, // but maybe maybe this should be a configurable option? - globalState.set({ - ...globalState.get(), - filters: filterManager.getGlobalFilters(), - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - } as S); + if (shouldSync()) { + globalState.set({ + ...globalState.get(), + filters: filterManager.getGlobalFilters(), + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + } const subs: Subscription[] = [ - global$.subscribe(newGlobalQueryState => { + global$.pipe(filter(shouldSync)).subscribe(newGlobalQueryState => { globalState.set({ ...globalState.get(), ...newGlobalQueryState }); }), globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { + updateInProgress = true; // cloneDeep is required because services are mutating passed objects // and state in state container is frozen time = time || timefilter.getTimeDefaults(); @@ -70,6 +95,8 @@ export const connectToQueryGlobalState = ( if (!compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)) { filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); } + + updateInProgress = false; }), ]; diff --git a/src/plugins/data/public/query/state_sync/types.ts b/src/plugins/data/public/query/state_sync/types.ts index 6b1b0d5066d58..7ed6bf6040af2 100644 --- a/src/plugins/data/public/query/state_sync/types.ts +++ b/src/plugins/data/public/query/state_sync/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters, RefreshInterval, TimeRange } from '../../../common'; +import { Filter, RefreshInterval, TimeRange } from '../../../common'; /** * State from data services that meant to be preserved between apps @@ -25,12 +25,12 @@ import { esFilters, RefreshInterval, TimeRange } from '../../../common'; export interface QueryGlobalState { time?: TimeRange; refreshInterval?: RefreshInterval; - filters?: esFilters.Filter[]; // pinned filters only + filters?: Filter[]; // pinned filters only } /** * State from data services that should be scope to a single app */ export interface QueryAppState { - filters?: esFilters.Filter[]; // not pinned filters + filters?: Filter[]; // not pinned filters } From 9b702218ecb7c1c8498a7b49e30bdae495c3346c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Sun, 16 Feb 2020 13:25:53 +0100 Subject: [PATCH 16/29] fix --- src/plugins/data/public/query/query_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index bf04d61a75801..1dd53e84e0752 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -82,7 +82,7 @@ export class QueryService { } public stop() { - // nothing here yet + // nothing to do here yet } } From 7be06131c34dba41abe75ab63fbae17afcb42be9 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Sun, 16 Feb 2020 16:01:57 +0100 Subject: [PATCH 17/29] jest -u --- .../query_string_input.test.tsx.snap | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 5d0cc45c8106d..f176b7d076e5b 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -191,7 +191,13 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { @@ -846,7 +852,13 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { @@ -1483,7 +1495,13 @@ exports[`QueryStringInput Should pass the query language to the language switche "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { @@ -2135,7 +2153,13 @@ exports[`QueryStringInput Should pass the query language to the language switche "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { @@ -2772,7 +2796,13 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { @@ -3424,7 +3454,13 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { + "app$": Observable { + "_isScalar": false, + }, "filterManager": [MockFunction], + "global$": Observable { + "_isScalar": false, + }, "savedQueries": [MockFunction], "timefilter": Object { "history": Object { From 19770c2cd234745d11856232e80e7dd4661d106e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 11:47:19 +0100 Subject: [PATCH 18/29] improve discover and visualise sub url tracking --- .../core_plugins/kibana/public/discover/plugin.ts | 12 ++---------- .../core_plugins/kibana/public/visualize/plugin.ts | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index e8ded9d99f892..84e628c2abbcf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; -import { - DataPublicPluginStart, - DataPublicPluginSetup, - getQueryStateContainer, -} from '../../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; @@ -103,9 +99,6 @@ export class DiscoverPlugin implements Plugin { public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { - const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( - plugins.data.query - ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', @@ -115,12 +108,11 @@ export class DiscoverPlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: querySyncStateContainer.state$, + stateUpdate$: plugins.data.query.global$, }, ], }); this.stopUrlTracking = () => { - stopQuerySyncStateContainer(); stopUrlTracker(); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 22804685db3cc..072a2ff026ada 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -30,11 +30,7 @@ import { } from 'kibana/public'; import { Storage, createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { - DataPublicPluginStart, - DataPublicPluginSetup, - getQueryStateContainer, -} from '../../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; @@ -84,9 +80,6 @@ export class VisualizePlugin implements Plugin { core: CoreSetup, { home, kibanaLegacy, usageCollection, data }: VisualizePluginSetupDependencies ) { - const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( - data.query - ); const { appMounted, appUnMounted, stop: stopUrlTracker, setActiveUrl } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/visualize', @@ -96,12 +89,11 @@ export class VisualizePlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: querySyncStateContainer.state$, + stateUpdate$: data.query.global$, }, ], }); this.stopUrlTracking = () => { - stopQuerySyncStateContainer(); stopUrlTracker(); }; From 63f7c8ca1251a08b6e1260589a1f8b06ac053ad7 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 12:28:23 +0100 Subject: [PATCH 19/29] update np karma mock --- src/legacy/ui/public/new_platform/new_platform.karma_mock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 38b3434ef9c48..2062698c3f972 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -104,6 +104,8 @@ export const npSetup = { getProvider: sinon.fake(), }, query: { + app$: mockObservable, + global$: mockObservable, filterManager: { getFetches$: sinon.fake(), getFilters: sinon.fake(), From 3e57279290fbd5f26ed1d5425350d3b4cbc2061e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 12:48:57 +0100 Subject: [PATCH 20/29] fix mock --- src/legacy/ui/public/new_platform/new_platform.karma_mock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 2062698c3f972..50a943e3ec273 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -104,8 +104,8 @@ export const npSetup = { getProvider: sinon.fake(), }, query: { - app$: mockObservable, - global$: mockObservable, + app$: mockObservable(), + global$: mockObservable(), filterManager: { getFetches$: sinon.fake(), getFilters: sinon.fake(), From d39d4915c5f0f0846a4eba77827ef5f6be54e8e4 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 17:14:20 +0100 Subject: [PATCH 21/29] wip --- .../with_data_services/components/app.tsx | 97 ++------ .../np_ready/dashboard_app_controller.tsx | 28 ++- .../public/dashboard/np_ready/legacy_app.js | 6 +- .../kibana/public/dashboard/plugin.ts | 7 +- .../kibana/public/discover/plugin.ts | 7 +- .../kibana/public/visualize/plugin.ts | 7 +- .../new_platform/new_platform.karma_mock.js | 3 +- src/plugins/data/public/index.ts | 8 +- src/plugins/data/public/query/mocks.ts | 6 +- .../data/public/query/query_service.ts | 21 +- .../state_sync/connect_to_app_state.test.ts | 222 ------------------ .../query/state_sync/connect_to_app_state.ts | 70 ------ .../state_sync/connect_to_global_state.ts | 106 --------- ...test.ts => connect_to_query_state.test.ts} | 195 ++++++++++++++- .../state_sync/connect_to_query_state.ts | 194 +++++++++++++++ .../state_sync/create_app_query_observable.ts | 61 ----- .../create_global_query_observable.ts | 58 +++-- .../data/public/query/state_sync/index.ts | 7 +- ...rl.test.ts => sync_state_with_url.test.ts} | 28 +-- ...ate_with_url.ts => sync_state_with_url.ts} | 25 +- .../data/public/query/state_sync/types.ts | 18 +- 21 files changed, 534 insertions(+), 640 deletions(-) delete mode 100644 src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts delete mode 100644 src/plugins/data/public/query/state_sync/connect_to_app_state.ts delete mode 100644 src/plugins/data/public/query/state_sync/connect_to_global_state.ts rename src/plugins/data/public/query/state_sync/{connect_to_global_state.test.ts => connect_to_query_state.test.ts} (60%) create mode 100644 src/plugins/data/public/query/state_sync/connect_to_query_state.ts delete mode 100644 src/plugins/data/public/query/state_sync/create_app_query_observable.ts rename src/plugins/data/public/query/state_sync/{sync_global_state_with_url.test.ts => sync_state_with_url.test.ts} (83%) rename src/plugins/data/public/query/state_sync/{sync_global_state_with_url.ts => sync_state_with_url.ts} (79%) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index f773dc5d5a12c..952c3e073de78 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -36,13 +36,14 @@ import { import { CoreStart } from '../../../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { - connectToQueryAppState, - connectToQueryGlobalState, + connectToQueryState, + syncQueryStateWithUrl, DataPublicPluginStart, IIndexPattern, - QueryAppState, - QueryGlobalState, + QueryState, QueryStart, + Filter, + esFilters, } from '../../../../../src/plugins/data/public'; import { BaseState, @@ -65,12 +66,14 @@ interface StateDemoAppDeps { kbnUrlStateStorage: IKbnUrlStateStorage; } -interface AppState extends QueryAppState { +interface AppState { name: string; + filters: Filter[]; // query?: Query; } const defaultAppState: AppState = { name: '', + filters: [], }; const { Provider: AppStateContainerProvider, @@ -78,18 +81,6 @@ const { useContainer: useAppStateContainer, } = createStateContainerReactHelpers>(); -interface GlobalState extends QueryGlobalState { - globalData: string; -} -const defaultGlobalState: GlobalState = { - globalData: '', -}; -const { - Provider: GlobalStateContainerProvider, - useState: useGlobalState, - useContainer: useGlobalStateContainer, -} = createStateContainerReactHelpers>(); - const App = ({ notifications, http, @@ -101,10 +92,7 @@ const App = ({ const appStateContainer = useAppStateContainer(); const appState = useAppState(); - const globalStateContainer = useGlobalStateContainer(); - const globalState = useGlobalState(); - - useGlobalStateSyncing(globalStateContainer, data.query, kbnUrlStateStorage); + useGlobalStateSyncing(data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); // const onQuerySubmit = useCallback( @@ -163,14 +151,6 @@ const App = ({ aria-label="My name" /> - - globalStateContainer.set({ ...globalState, globalData: e.target.value }) - } - aria-label="My global data" - /> @@ -182,13 +162,10 @@ const App = ({ export const StateDemoApp = (props: StateDemoAppDeps) => { const appStateContainer = useCreateStateContainer(defaultAppState); - const globalStateContainer = useCreateStateContainer(defaultGlobalState); return ( - - - + ); }; @@ -218,55 +195,18 @@ function useIndexPattern(data: DataPublicPluginStart) { return indexPattern; } -function useGlobalStateSyncing( - globalStateContainer: BaseStateContainer, - query: QueryStart, - kbnUrlStateStorage: IKbnUrlStateStorage -) { +function useGlobalStateSyncing(query: QueryStart, kbnUrlStateStorage: IKbnUrlStateStorage) { // setup sync state utils useEffect(() => { - // sync global filters, time filters, refresh interval from data.query to state container - const stopSyncingQueryGlobalStateWithStateContainer = connectToQueryGlobalState( - query, - globalStateContainer - ); - - // sets up syncing global state container with url - const { - start: startSyncingGlobalStateWithUrl, - stop: stopSyncingGlobalStateWithUrl, - } = syncState({ - storageKey: '_g', - stateStorage: kbnUrlStateStorage, - stateContainer: { - ...globalStateContainer, - // stateSync utils requires explicit handling of default state ("null") - set: state => state && globalStateContainer.set(state), - }, - }); - - // merge initial state from global state container and current state in url - const initialGlobalState: GlobalState = { - ...globalStateContainer.get(), - ...kbnUrlStateStorage.get('_g'), - }; - // trigger state update. actually needed in case some data was in url - globalStateContainer.set(initialGlobalState); - - // set current url to whatever is in global state container - kbnUrlStateStorage.set('_g', initialGlobalState); - - // finally start syncing state containers with url - startSyncingGlobalStateWithUrl(); - + // sync global filters, time filters, refresh interval from data.query to url '_g' + const { stop } = syncQueryStateWithUrl(query, kbnUrlStateStorage); return () => { - stopSyncingQueryGlobalStateWithStateContainer(); - stopSyncingGlobalStateWithUrl(); + stop(); }; - }, [query, kbnUrlStateStorage, globalStateContainer]); + }, [query, kbnUrlStateStorage]); } -function useAppStateSyncing( +function useAppStateSyncing( appStateContainer: BaseStateContainer, query: QueryStart, kbnUrlStateStorage: IKbnUrlStateStorage @@ -274,9 +214,10 @@ function useAppStateSyncing( // setup sync state utils useEffect(() => { // sync app filters with app state container from data.query to state container - const stopSyncingQueryAppStateWithStateContainer = connectToQueryAppState( + const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( query, - appStateContainer + appStateContainer, + { filters: esFilters.FilterStateStore.APP_STATE } ); // sets up syncing app state container with url diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 69821c0013b26..a16d5880d5ed2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -30,18 +30,18 @@ import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_emp import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; import { + connectToQueryState, esFilters, IndexPattern, IndexPatternsContract, Query, SavedQuery, - syncGlobalQueryStateWithUrl, - connectToQueryAppState, + syncQueryStateWithUrl, } from '../../../../../../plugins/data/public'; import { + getSavedObjectFinder, SaveResult, showSaveModal, - getSavedObjectFinder, } from '../../../../../../plugins/saved_objects/public'; import { @@ -128,9 +128,9 @@ export class DashboardAppController { // starts syncing `_g` portion of url with query services // note: dashboard_state_manager.ts syncs `_a` portion of url const { - stop: stopSyncingGlobalStateWithUrl, + stop: stopSyncingQueryServiceStateWithUrl, hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl, - } = syncGlobalQueryStateWithUrl(queryService, kbnUrlStateStorage); + } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); let lastReloadRequestTime = 0; @@ -150,11 +150,17 @@ export class DashboardAppController { // sync initial app filters from state to filterManager filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters)); // setup syncing of app filters between appState and filterManager - const stopSyncingAppFilters = connectToQueryAppState(queryService, { - set: ({ filters }) => dashboardStateManager.setFilters(filters || []), - get: () => ({ filters: dashboardStateManager.appState.filters }), - state$: dashboardStateManager.appState$.pipe(map(state => ({ filters: state.filters }))), - }); + const stopSyncingAppFilters = connectToQueryState( + queryService, + { + set: ({ filters }) => dashboardStateManager.setFilters(filters || []), + get: () => ({ filters: dashboardStateManager.appState.filters }), + state$: dashboardStateManager.appState$.pipe(map(state => ({ filters: state.filters }))), + }, + { + filters: esFilters.FilterStateStore.APP_STATE, + } + ); // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. @@ -901,7 +907,7 @@ export class DashboardAppController { $scope.$on('$destroy', () => { updateSubscription.unsubscribe(); - stopSyncingGlobalStateWithUrl(); + stopSyncingQueryServiceStateWithUrl(); stopSyncingAppFilters(); visibleSubscription.unsubscribe(); $scope.timefilterSubscriptions$.unsubscribe(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 1322821ba2ed6..35b510894179d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -33,7 +33,7 @@ import { } from '../../../../../../plugins/kibana_utils/public'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; -import { syncGlobalQueryStateWithUrl } from '../../../../../../plugins/data/public'; +import { syncQueryStateWithUrl } from '../../../../../../plugins/data/public'; export function initDashboardApp(app, deps) { initDashboardAppDirective(app, deps); @@ -98,7 +98,7 @@ export function initDashboardApp(app, deps) { const dashboardConfig = deps.dashboardConfig; // syncs `_g` portion of url with query services - const { stop: stopSyncingGlobalStateWithUrl } = syncGlobalQueryStateWithUrl( + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( deps.data.query, kbnUrlStateStorage ); @@ -132,7 +132,7 @@ export function initDashboardApp(app, deps) { $scope.core = deps.core; $scope.$on('$destroy', () => { - stopSyncingGlobalStateWithUrl(); + stopSyncingQueryServiceStateWithUrl(); }); }, resolve: { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 2ee8c3b2b37ea..28fc4edc548c1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { App, AppMountParameters, @@ -86,7 +87,11 @@ export class DashboardPlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: data.query.global$, + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ) + ), }, ], }); diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 84e628c2abbcf..b7a01e1e714d8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; @@ -108,7 +109,11 @@ export class DiscoverPlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: plugins.data.query.global$, + stateUpdate$: plugins.data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ) + ), }, ], }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 072a2ff026ada..56945971b4f03 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { i18n } from '@kbn/i18n'; +import { filter } from 'rxjs/operators'; import { AppMountParameters, @@ -89,7 +90,11 @@ export class VisualizePlugin implements Plugin { stateParams: [ { kbnUrlKey: '_g', - stateUpdate$: data.query.global$, + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ) + ), }, ], }); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 50a943e3ec273..117a91738226b 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -104,8 +104,7 @@ export const npSetup = { getProvider: sinon.fake(), }, query: { - app$: mockObservable(), - global$: mockObservable(), + state$: mockObservable(), filterManager: { getFetches$: sinon.fake(), getFilters: sinon.fake(), diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index a525e94405fcc..8ff2e25cc2403 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -291,11 +291,9 @@ export { Filter, Query, RefreshInterval, TimeRange } from '../common'; export { createSavedQueryService, - connectToQueryGlobalState, - connectToQueryAppState, - syncGlobalQueryStateWithUrl, - QueryGlobalState, - QueryAppState, + connectToQueryState, + syncQueryStateWithUrl, + QueryState, QueryStart, getTime, getQueryLog, diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index e6b079a0f4632..47b0a5b871ce2 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -27,8 +27,7 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { filterManager: jest.fn() as any, timefilter: timefilterServiceMock.createSetupContract(), - global$: new Observable(), - app$: new Observable(), + state$: new Observable(), }; return setupContract; @@ -39,8 +38,7 @@ const createStartContractMock = () => { filterManager: jest.fn() as any, timefilter: timefilterServiceMock.createStartContract(), savedQueries: jest.fn() as any, - global$: new Observable(), - app$: new Observable(), + state$: new Observable(), }; return startContract; diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 1dd53e84e0752..c885d596f1943 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -17,16 +17,13 @@ * under the License. */ -import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { FilterManager } from './filter_manager'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; -import { QueryAppState, QueryGlobalState } from './state_sync'; -import { createGlobalQueryObservable } from './state_sync/create_global_query_observable'; -import { createAppQueryObservable } from './state_sync/create_app_query_observable'; +import { createQueryStateObservable } from './state_sync/create_global_query_observable'; /** * Query Service @@ -41,8 +38,7 @@ export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; - app$!: Observable; - global$!: Observable; + state$!: ReturnType; public setup({ uiSettings, storage }: QueryServiceDependencies) { this.filterManager = new FilterManager(uiSettings); @@ -53,12 +49,7 @@ export class QueryService { storage, }); - this.global$ = createGlobalQueryObservable({ - filterManager: this.filterManager, - timefilter: this.timefilter, - }).pipe(share()); - - this.app$ = createAppQueryObservable({ + this.state$ = createQueryStateObservable({ filterManager: this.filterManager, timefilter: this.timefilter, }).pipe(share()); @@ -66,8 +57,7 @@ export class QueryService { return { filterManager: this.filterManager, timefilter: this.timefilter, - global$: this.global$, - app$: this.app$, + state$: this.state$, }; } @@ -75,8 +65,7 @@ export class QueryService { return { filterManager: this.filterManager, timefilter: this.timefilter, - global$: this.global$, - app$: this.app$, + state$: this.state$, savedQueries: createSavedQueryService(savedObjects.client), }; } diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts deleted file mode 100644 index 1b0deb26ad991..0000000000000 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Subscription } from 'rxjs'; -import { FilterManager } from '../filter_manager'; -import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore } from '../../../common'; -import { connectToQueryAppState } from './connect_to_app_state'; -import { coreMock } from '../../../../../core/public/mocks'; -import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; -import { QueryService, QueryStart } from '../query_service'; -import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; -import { QueryAppState } from './types'; - -const setupMock = coreMock.createSetup(); -const startMock = coreMock.createStart(); - -setupMock.uiSettings.get.mockImplementation((key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return true; - case 'timepicker:timeDefaults': - return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`sync_query test: not mocked uiSetting: ${key}`); - } -}); - -describe('connect_to_app_state', () => { - let queryServiceStart: QueryStart; - let filterManager: FilterManager; - let appState: BaseStateContainer; - let appStateSub: Subscription; - let appStateChangeTriggered = jest.fn(); - let filterManagerChangeSub: Subscription; - let filterManagerChangeTriggered = jest.fn(); - - let gF1: Filter; - let gF2: Filter; - let aF1: Filter; - let aF2: Filter; - - beforeEach(() => { - const queryService = new QueryService(); - queryService.setup({ - uiSettings: setupMock.uiSettings, - storage: new Storage(new StubBrowserStorage()), - }); - queryServiceStart = queryService.start(startMock.savedObjects); - filterManager = queryServiceStart.filterManager; - - appState = createStateContainer({}); - appStateChangeTriggered = jest.fn(); - appStateSub = appState.state$.subscribe(appStateChangeTriggered); - - filterManagerChangeTriggered = jest.fn(); - filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered); - - gF1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1'); - gF2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'key2', 'value2'); - aF1 = getFilter(FilterStateStore.APP_STATE, true, true, 'key3', 'value3'); - aF2 = getFilter(FilterStateStore.APP_STATE, false, false, 'key4', 'value4'); - }); - afterEach(() => { - appStateSub.unsubscribe(); - filterManagerChangeSub.unsubscribe(); - }); - - describe('sync from filterManager to app state', () => { - test('should sync app filters to app state when new app filters set to filterManager', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - - filterManager.setFilters([gF1, aF1]); - - expect(appState.get().filters).toHaveLength(1); - stop(); - }); - - test('should not sync global filters to app state ', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - - filterManager.setFilters([gF1, gF2]); - - expect(appState.get().filters).toHaveLength(0); - stop(); - }); - - test("should not trigger changes when app filters didn't change", () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); - - filterManager.setFilters([gF1, aF1]); - filterManager.setFilters([gF2, aF1]); - - expect(appStateChangeTriggered).toBeCalledTimes(1); - expect(appState.get().filters).toHaveLength(1); - - stop(); - }); - - test('should trigger changes when app filters change', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); - - filterManager.setFilters([gF1, aF1]); - filterManager.setFilters([gF1, aF2]); - - expect(appStateChangeTriggered).toBeCalledTimes(2); - expect(appState.get().filters).toHaveLength(1); - - stop(); - }); - - test('resetting filters should sync to app state', () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - - filterManager.setFilters([gF1, aF1]); - - expect(appState.get().filters).toHaveLength(1); - - filterManager.removeAll(); - - expect(appState.get().filters).toHaveLength(0); - - stop(); - }); - - test("shouldn't sync filters when syncing is stopped", () => { - const stop = connectToQueryAppState(queryServiceStart, appState); - - filterManager.setFilters([gF1, aF1]); - - expect(appState.get().filters).toHaveLength(1); - - stop(); - - filterManager.removeAll(); - - expect(appState.get().filters).toHaveLength(1); - }); - - test('should pick up initial state from filterManager', () => { - appState.set({ filters: [aF1] }); - filterManager.setFilters([gF1]); - - appStateChangeTriggered.mockClear(); - const stop = connectToQueryAppState(queryServiceStart, appState); - expect(appStateChangeTriggered).toBeCalledTimes(1); - expect(appState.get().filters).toHaveLength(0); - - stop(); - }); - }); - describe('sync from app state to filterManager', () => { - test('changes to app state should be synced to app filters', () => { - filterManager.setFilters([gF1]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); - - appState.set({ filters: [aF1] }); - - expect(filterManager.getFilters()).toHaveLength(2); - expect(filterManager.getAppFilters()).toHaveLength(1); - expect(filterManager.getGlobalFilters()).toHaveLength(1); - expect(appStateChangeTriggered).toBeCalledTimes(1); - stop(); - }); - - test('global filters should remain untouched', () => { - filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appStateChangeTriggered.mockClear(); - - appState.set({ filters: [] }); - - expect(filterManager.getFilters()).toHaveLength(2); - expect(filterManager.getGlobalFilters()).toHaveLength(2); - expect(appStateChangeTriggered).toBeCalledTimes(1); - stop(); - }); - - test("if filters are not changed, filterManager shouldn't trigger update", () => { - filterManager.setFilters([gF1, gF2, aF1, aF2]); - filterManagerChangeTriggered.mockClear(); - - appState.set({ filters: [aF1, aF2] }); - const stop = connectToQueryAppState(queryServiceStart, appState); - appState.set({ filters: [aF1, aF2] }); - - expect(filterManagerChangeTriggered).toBeCalledTimes(0); - stop(); - }); - - test('stop() should stop syncing', () => { - filterManager.setFilters([gF1, gF2, aF1, aF2]); - const stop = connectToQueryAppState(queryServiceStart, appState); - appState.set({ filters: [] }); - expect(filterManager.getFilters()).toHaveLength(2); - stop(); - appState.set({ filters: [aF1] }); - expect(filterManager.getFilters()).toHaveLength(2); - }); - }); -}); diff --git a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts b/src/plugins/data/public/query/state_sync/connect_to_app_state.ts deleted file mode 100644 index 2bc76fd0d3296..0000000000000 --- a/src/plugins/data/public/query/state_sync/connect_to_app_state.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { filter } from 'rxjs/operators'; -import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; -import { BaseStateContainer } from '../../../../../plugins/kibana_utils/public'; -import { QuerySetup, QueryStart } from '../query_service'; -import { QueryAppState } from './types'; - -/** - * Helper to setup two-way syncing of app scoped data and a state container - * @param QueryService: either setup or start - * @param stateContainer to use for syncing - */ -export function connectToQueryAppState( - { filterManager, app$ }: Pick, - appState: BaseStateContainer -) { - function shouldSync() { - const stateContainerFilters = appState.get().filters; - if (!stateContainerFilters) return true; - const filterManagerFilters = filterManager.getAppFilters(); - const areAppFiltersEqual = compareFilters( - stateContainerFilters, - filterManagerFilters, - COMPARE_ALL_OPTIONS - ); - - return !areAppFiltersEqual; - } - - // initial syncing - // TODO: - // filterManager takes precedence, this seems like a good default, - // and apps could anyway set their own value after initialisation, - // but maybe maybe this should be a configurable option? - if (shouldSync()) { - appState.set({ ...appState.get(), filters: filterManager.getAppFilters() }); - } - - const subs = [ - app$.pipe(filter(shouldSync)).subscribe(appQueryState => { - appState.set({ ...appState.get(), ...appQueryState }); - }), - appState.state$.pipe(filter(shouldSync)).subscribe(appFilters => { - filterManager.setAppFilters(_.cloneDeep(appFilters.filters || [])); - }), - ]; - - return () => { - subs.forEach(s => s.unsubscribe()); - }; -} diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts b/src/plugins/data/public/query/state_sync/connect_to_global_state.ts deleted file mode 100644 index 2bd2d1312a5a9..0000000000000 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Subscription } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import _ from 'lodash'; -import { BaseStateContainer } from '../../../../kibana_utils/public'; -import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; -import { QuerySetup, QueryStart } from '../query_service'; -import { QueryGlobalState } from './types'; - -/** - * Helper to setup two-way syncing of global data and a state container - * @param QueryService: either setup or start - * @param stateContainer to use for syncing - */ -export const connectToQueryGlobalState = ( - { - timefilter: { timefilter }, - filterManager, - global$, - }: Pick, - globalState: BaseStateContainer -) => { - let updateInProgress = false; - function shouldSync() { - if (updateInProgress) return false; - const { filters, refreshInterval, time } = globalState.get(); - if (!filters) return true; - const areGlobalFiltersEqual = compareFilters( - filters, - filterManager.getGlobalFilters(), - COMPARE_ALL_OPTIONS - ); - if (!areGlobalFiltersEqual) return true; - - const areRefreshIntervalsEqual = _.isEqual(refreshInterval, timefilter.getRefreshInterval()); - if (!areRefreshIntervalsEqual) return true; - - const areTimesEqual = _.isEqual(time, timefilter.getTime()); - if (!areTimesEqual) return true; - - return false; - } - - // initial syncing - // TODO: - // data services take precedence, this seems like a good default, - // and apps could anyway set their own value after initialisation, - // but maybe maybe this should be a configurable option? - if (shouldSync()) { - globalState.set({ - ...globalState.get(), - filters: filterManager.getGlobalFilters(), - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }); - } - - const subs: Subscription[] = [ - global$.pipe(filter(shouldSync)).subscribe(newGlobalQueryState => { - globalState.set({ ...globalState.get(), ...newGlobalQueryState }); - }), - globalState.state$.subscribe(({ time, filters: globalFilters, refreshInterval }) => { - updateInProgress = true; - // cloneDeep is required because services are mutating passed objects - // and state in state container is frozen - time = time || timefilter.getTimeDefaults(); - if (!_.isEqual(time, timefilter.getTime())) { - timefilter.setTime(_.cloneDeep(time)); - } - - refreshInterval = refreshInterval || timefilter.getRefreshIntervalDefaults(); - if (!_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { - timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); - } - - globalFilters = globalFilters || []; - if (!compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)) { - filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); - } - - updateInProgress = false; - }), - ]; - - return () => { - subs.forEach(s => s.unsubscribe()); - }; -}; diff --git a/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts similarity index 60% rename from src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts rename to src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 3088cf8717f36..5da929c441cde 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_global_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -25,9 +25,21 @@ import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, Storage } from '../../../../kibana_utils/public'; import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; -import { connectToQueryGlobalState } from './connect_to_global_state'; +import { connectToQueryState } from './connect_to_query_state'; import { TimefilterContract } from '../timefilter'; -import { QueryGlobalState } from './types'; +import { QueryState } from './types'; + +const connectToQueryGlobalState = (query: QueryStart, state: BaseStateContainer) => + connectToQueryState(query, state, { + refreshInterval: true, + time: true, + filters: FilterStateStore.GLOBAL_STATE, + }); + +const connectToQueryAppState = (query: QueryStart, state: BaseStateContainer) => + connectToQueryState(query, state, { + filters: FilterStateStore.APP_STATE, + }); const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -49,7 +61,7 @@ describe('connect_to_global_state', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; let timeFilter: TimefilterContract; - let globalState: BaseStateContainer; + let globalState: BaseStateContainer; let globalStateSub: Subscription; let globalStateChangeTriggered = jest.fn(); let filterManagerChangeSub: Subscription; @@ -274,3 +286,180 @@ describe('connect_to_global_state', () => { }); }); }); + +describe('connect_to_app_state', () => { + let queryServiceStart: QueryStart; + let filterManager: FilterManager; + let appState: BaseStateContainer; + let appStateSub: Subscription; + let appStateChangeTriggered = jest.fn(); + let filterManagerChangeSub: Subscription; + let filterManagerChangeTriggered = jest.fn(); + + let gF1: Filter; + let gF2: Filter; + let aF1: Filter; + let aF2: Filter; + + beforeEach(() => { + const queryService = new QueryService(); + queryService.setup({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + }); + queryServiceStart = queryService.start(startMock.savedObjects); + filterManager = queryServiceStart.filterManager; + + appState = createStateContainer({}); + appStateChangeTriggered = jest.fn(); + appStateSub = appState.state$.subscribe(appStateChangeTriggered); + + filterManagerChangeTriggered = jest.fn(); + filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered); + + gF1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1'); + gF2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'key2', 'value2'); + aF1 = getFilter(FilterStateStore.APP_STATE, true, true, 'key3', 'value3'); + aF2 = getFilter(FilterStateStore.APP_STATE, false, false, 'key4', 'value4'); + }); + afterEach(() => { + appStateSub.unsubscribe(); + filterManagerChangeSub.unsubscribe(); + }); + + describe('sync from filterManager to app state', () => { + test('should sync app filters to app state when new app filters set to filterManager', () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + + filterManager.setFilters([gF1, aF1]); + + expect(appState.get().filters).toHaveLength(1); + stop(); + }); + + test('should not sync global filters to app state ', () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + + filterManager.setFilters([gF1, gF2]); + + expect(appState.get().filters).toHaveLength(0); + stop(); + }); + + test("should not trigger changes when app filters didn't change", () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); + + filterManager.setFilters([gF1, aF1]); + filterManager.setFilters([gF2, aF1]); + + expect(appStateChangeTriggered).toBeCalledTimes(1); + expect(appState.get().filters).toHaveLength(1); + + stop(); + }); + + test('should trigger changes when app filters change', () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); + + filterManager.setFilters([gF1, aF1]); + filterManager.setFilters([gF1, aF2]); + + expect(appStateChangeTriggered).toBeCalledTimes(2); + expect(appState.get().filters).toHaveLength(1); + + stop(); + }); + + test('resetting filters should sync to app state', () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + + filterManager.setFilters([gF1, aF1]); + + expect(appState.get().filters).toHaveLength(1); + + filterManager.removeAll(); + + expect(appState.get().filters).toHaveLength(0); + + stop(); + }); + + test("shouldn't sync filters when syncing is stopped", () => { + const stop = connectToQueryAppState(queryServiceStart, appState); + + filterManager.setFilters([gF1, aF1]); + + expect(appState.get().filters).toHaveLength(1); + + stop(); + + filterManager.removeAll(); + + expect(appState.get().filters).toHaveLength(1); + }); + + test('should pick up initial state from filterManager', () => { + appState.set({ filters: [aF1] }); + filterManager.setFilters([gF1]); + + appStateChangeTriggered.mockClear(); + const stop = connectToQueryAppState(queryServiceStart, appState); + expect(appStateChangeTriggered).toBeCalledTimes(1); + expect(appState.get().filters).toHaveLength(0); + + stop(); + }); + }); + describe('sync from app state to filterManager', () => { + test('changes to app state should be synced to app filters', () => { + filterManager.setFilters([gF1]); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); + + appState.set({ filters: [aF1] }); + + expect(filterManager.getFilters()).toHaveLength(2); + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(appStateChangeTriggered).toBeCalledTimes(1); + stop(); + }); + + test('global filters should remain untouched', () => { + filterManager.setFilters([gF1, gF2, aF1, aF2]); + const stop = connectToQueryAppState(queryServiceStart, appState); + appStateChangeTriggered.mockClear(); + + appState.set({ filters: [] }); + + expect(filterManager.getFilters()).toHaveLength(2); + expect(filterManager.getGlobalFilters()).toHaveLength(2); + expect(appStateChangeTriggered).toBeCalledTimes(1); + stop(); + }); + + test("if filters are not changed, filterManager shouldn't trigger update", () => { + filterManager.setFilters([gF1, gF2, aF1, aF2]); + filterManagerChangeTriggered.mockClear(); + + appState.set({ filters: [aF1, aF2] }); + const stop = connectToQueryAppState(queryServiceStart, appState); + appState.set({ filters: [aF1, aF2] }); + + expect(filterManagerChangeTriggered).toBeCalledTimes(0); + stop(); + }); + + test('stop() should stop syncing', () => { + filterManager.setFilters([gF1, gF2, aF1, aF2]); + const stop = connectToQueryAppState(queryServiceStart, appState); + appState.set({ filters: [] }); + expect(filterManager.getFilters()).toHaveLength(2); + stop(); + appState.set({ filters: [aF1] }); + expect(filterManager.getFilters()).toHaveLength(2); + }); + }); +}); diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts new file mode 100644 index 0000000000000..a22e66860c765 --- /dev/null +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -0,0 +1,194 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import _ from 'lodash'; +import { BaseStateContainer } from '../../../../kibana_utils/public'; +import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; +import { QuerySetup, QueryStart } from '../query_service'; +import { QueryState, QueryStateChange } from './types'; +import { FilterStateStore } from '../../../common/es_query/filters'; + +/** + * Helper to setup two-way syncing of global data and a state container + * @param QueryService: either setup or start + * @param stateContainer to use for syncing + */ +export const connectToQueryState = ( + { + timefilter: { timefilter }, + filterManager, + state$, + }: Pick, + stateContainer: BaseStateContainer, + syncConfig: { time?: boolean; refreshInterval?: boolean; filters?: FilterStateStore | boolean } +) => { + const syncKeys: Array = []; + if (syncConfig.time) { + syncKeys.push('time'); + } + if (syncConfig.refreshInterval) { + syncKeys.push('refreshInterval'); + } + if (syncConfig.filters) { + switch (syncConfig.filters) { + case true: + syncKeys.push('filters'); + break; + case FilterStateStore.APP_STATE: + syncKeys.push('appFilters'); + break; + case FilterStateStore.GLOBAL_STATE: + syncKeys.push('globalFilters'); + break; + } + } + + // initial syncing + // TODO: + // data services take precedence, this seems like a good default, + // and apps could anyway set their own value after initialisation, + // but maybe maybe this should be a configurable option? + const initialState: QueryState = { ...stateContainer.get() }; + let initialDirty = false; + if (syncConfig.time && !_.isEqual(initialState.time, timefilter.getTime())) { + initialState.time = timefilter.getTime(); + initialDirty = true; + } + if ( + syncConfig.refreshInterval && + !_.isEqual(initialState.refreshInterval, timefilter.getRefreshInterval()) + ) { + initialState.refreshInterval = timefilter.getRefreshInterval(); + initialDirty = true; + } + + if (syncConfig.filters) { + if (syncConfig.filters === true) { + if ( + !initialState.filters || + !compareFilters(initialState.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS) + ) { + initialState.filters = filterManager.getFilters(); + initialDirty = true; + } + } else if (syncConfig.filters === FilterStateStore.GLOBAL_STATE) { + if ( + !initialState.filters || + !compareFilters(initialState.filters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) + ) { + initialState.filters = filterManager.getGlobalFilters(); + initialDirty = true; + } + } else if (syncConfig.filters === FilterStateStore.APP_STATE) { + if ( + !initialState.filters || + !compareFilters(initialState.filters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS) + ) { + initialState.filters = filterManager.getAppFilters(); + initialDirty = true; + } + } + } + + if (initialDirty) { + stateContainer.set({ ...stateContainer.get(), ...initialState }); + } + + // to ignore own state updates + let updateInProgress = false; + + const subs: Subscription[] = [ + state$ + .pipe( + filter(({ changes, state }) => { + if (updateInProgress) return false; + return syncKeys.some(syncKey => changes[syncKey]); + }), + map(({ changes }) => { + const newState: QueryState = {}; + if (syncConfig.time && changes.time) { + newState.time = timefilter.getTime(); + } + if (syncConfig.refreshInterval && changes.refreshInterval) { + newState.refreshInterval = timefilter.getRefreshInterval(); + } + if (syncConfig.filters) { + if (syncConfig.filters === true && changes.filters) { + newState.filters = filterManager.getFilters(); + } else if ( + syncConfig.filters === FilterStateStore.GLOBAL_STATE && + changes.globalFilters + ) { + newState.filters = filterManager.getGlobalFilters(); + } else if (syncConfig.filters === FilterStateStore.APP_STATE && changes.appFilters) { + newState.filters = filterManager.getAppFilters(); + } + } + return newState; + }) + ) + .subscribe(newState => { + stateContainer.set({ ...stateContainer.get(), ...newState }); + }), + stateContainer.state$.subscribe(state => { + updateInProgress = true; + + // cloneDeep is required because services are mutating passed objects + // and state in state container is frozen + if (syncConfig.time) { + const time = state.time || timefilter.getTimeDefaults(); + if (!_.isEqual(time, timefilter.getTime())) { + timefilter.setTime(_.cloneDeep(time)); + } + } + + if (syncConfig.refreshInterval) { + const refreshInterval = state.refreshInterval || timefilter.getRefreshIntervalDefaults(); + if (!_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { + timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); + } + } + + if (syncConfig.filters) { + const filters = state.filters || []; + if (syncConfig.filters === true) { + if (!compareFilters(filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { + filterManager.setFilters(_.cloneDeep(filters)); + } + } else if (syncConfig.filters === FilterStateStore.APP_STATE) { + if (!compareFilters(filters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS)) { + filterManager.setAppFilters(_.cloneDeep(filters)); + } + } else if (syncConfig.filters === FilterStateStore.GLOBAL_STATE) { + if (!compareFilters(filters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)) { + filterManager.setGlobalFilters(_.cloneDeep(filters)); + } + } + } + + updateInProgress = false; + }), + ]; + + return () => { + subs.forEach(s => s.unsubscribe()); + }; +}; diff --git a/src/plugins/data/public/query/state_sync/create_app_query_observable.ts b/src/plugins/data/public/query/state_sync/create_app_query_observable.ts deleted file mode 100644 index 770f894384202..0000000000000 --- a/src/plugins/data/public/query/state_sync/create_app_query_observable.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Observable, Subscription } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; -import { TimefilterSetup } from '../timefilter'; -import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; -import { QueryAppState } from './index'; -import { createStateContainer } from '../../../../kibana_utils/common/state_containers'; - -export function createAppQueryObservable({ - timefilter: { timefilter }, - filterManager, -}: { - timefilter: TimefilterSetup; - filterManager: FilterManager; -}): Observable { - return new Observable(subscriber => { - const state = createStateContainer({ - filters: filterManager.getAppFilters(), - }); - - const subs: Subscription[] = [ - filterManager - .getUpdates$() - .pipe( - // we need to track only app filters here - map(() => filterManager.getAppFilters()), - // continue only if app filters changed - // and ignore global state filters - filter( - newAppFilters => - !compareFilters(newAppFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) - ) - ) - .subscribe(newAppFilters => { - state.set({ ...state.get(), filters: newAppFilters }); - }), - state.state$.subscribe(subscriber), - ]; - return () => { - subs.forEach(s => s.unsubscribe()); - }; - }); -} diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index 6aabc6fdeb09b..ed3f4d7473b3d 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -18,49 +18,67 @@ */ import { Observable, Subscription } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { TimefilterSetup } from '../timefilter'; import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; -import { QueryGlobalState } from './index'; +import { QueryState, QueryStateChange } from './index'; import { createStateContainer } from '../../../../kibana_utils/public'; +import { FilterStateStore } from '../../../common/es_query/filters'; -export function createGlobalQueryObservable({ +export function createQueryStateObservable({ timefilter: { timefilter }, filterManager, }: { timefilter: TimefilterSetup; filterManager: FilterManager; -}): Observable { +}): Observable<{ changes: QueryStateChange; state: QueryState }> { return new Observable(subscriber => { - const state = createStateContainer({ + const state = createStateContainer({ time: timefilter.getTime(), refreshInterval: timefilter.getRefreshInterval(), - filters: filterManager.getGlobalFilters(), + filters: filterManager.getFilters(), }); + let currentChange: QueryStateChange = {}; const subs: Subscription[] = [ timefilter.getTimeUpdate$().subscribe(() => { + currentChange.time = true; state.set({ ...state.get(), time: timefilter.getTime() }); }), timefilter.getRefreshIntervalUpdate$().subscribe(() => { + currentChange.refreshInterval = true; state.set({ ...state.get(), refreshInterval: timefilter.getRefreshInterval() }); }), - filterManager - .getUpdates$() + filterManager.getUpdates$().subscribe(() => { + currentChange.filters = true; + + const { filters } = state.get(); + const globalOld = filters?.filter(f => f?.$state?.store === FilterStateStore.GLOBAL_STATE); + const appOld = filters?.filter(f => f?.$state?.store === FilterStateStore.APP_STATE); + const globalNew = filterManager.getGlobalFilters(); + const appNew = filterManager.getAppFilters(); + + if (!globalOld || !compareFilters(globalOld, globalNew, COMPARE_ALL_OPTIONS)) { + currentChange.globalFilters = true; + } + + if (!appOld || !compareFilters(appOld, appNew, COMPARE_ALL_OPTIONS)) { + currentChange.appFilters = true; + } + + state.set({ + ...state.get(), + filters: filterManager.getFilters(), + }); + }), + state.state$ .pipe( - // we need to track only global filters here - map(() => filterManager.getGlobalFilters()), - // continue only if global filters changed - // and ignore app state filters - filter( - newGlobalFilters => - !compareFilters(newGlobalFilters, state.get().filters || [], COMPARE_ALL_OPTIONS) - ) + map(newState => ({ state: newState, changes: currentChange })), + tap(() => { + currentChange = {}; + }) ) - .subscribe(newGlobalFilters => { - state.set({ ...state.get(), filters: newGlobalFilters }); - }), - state.state$.subscribe(subscriber), + .subscribe(subscriber), ]; return () => { subs.forEach(s => s.unsubscribe()); diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index dfdf4d6d0f210..e1a3561e022db 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -17,7 +17,6 @@ * under the License. */ -export { connectToQueryGlobalState } from './connect_to_global_state'; -export { connectToQueryAppState } from './connect_to_app_state'; -export { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; -export { QueryAppState, QueryGlobalState } from './types'; +export { connectToQueryState } from './connect_to_query_state'; +export { syncQueryStateWithUrl } from './sync_state_with_url'; +export { QueryState, QueryStateChange } from './types'; diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts similarity index 83% rename from src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts rename to src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 0f01856c85a32..50dc35ea955ee 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -31,8 +31,8 @@ import { import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { TimefilterContract } from '../timefilter'; -import { syncGlobalQueryStateWithUrl } from './sync_global_state_with_url'; -import { QueryGlobalState } from './types'; +import { syncQueryStateWithUrl } from './sync_state_with_url'; +import { QueryState } from './types'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -50,7 +50,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { } }); -describe('sync_global_state_with_url', () => { +describe('sync_query_state_with_url', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; let timefilter: TimefilterContract; @@ -91,7 +91,7 @@ describe('sync_global_state_with_url', () => { }); test('url is actually changed when data in services changes', () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); kbnUrlStateStorage.flush(); // sync force location change expect(history.location.hash).toMatchInlineSnapshot( @@ -101,16 +101,16 @@ describe('sync_global_state_with_url', () => { }); test('when filters change, global filters synced to urlStorage', () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); - expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); + expect(kbnUrlStateStorage.get('_g')?.filters).toHaveLength(1); stop(); }); test('when time range changes, time synced to urlStorage', () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setTime({ from: 'now-30m', to: 'now' }); - expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.time).toEqual({ from: 'now-30m', to: 'now', }); @@ -118,9 +118,9 @@ describe('sync_global_state_with_url', () => { }); test('when refresh interval changes, refresh interval is synced to urlStorage', () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); timefilter.setRefreshInterval({ pause: true, value: 100 }); - expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ + expect(kbnUrlStateStorage.get('_g')?.refreshInterval).toEqual({ pause: true, value: 100, }); @@ -128,7 +128,7 @@ describe('sync_global_state_with_url', () => { }); test('when url is changed, filters synced back to filterManager', () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); kbnUrlStateStorage.cancel(); // stop initial syncing pending update history.push(pathWithFilter); expect(filterManager.getGlobalFilters()).toHaveLength(1); @@ -138,7 +138,7 @@ describe('sync_global_state_with_url', () => { test('initial url should be synced with services', () => { history.push(pathWithFilter); - const { stop, hasInheritedQueryFromUrl } = syncGlobalQueryStateWithUrl( + const { stop, hasInheritedQueryFromUrl } = syncQueryStateWithUrl( queryServiceStart, kbnUrlStateStorage ); @@ -148,7 +148,7 @@ describe('sync_global_state_with_url', () => { }); test("url changes shouldn't trigger services updates if data didn't change", () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManagerChangeTriggered.mockClear(); history.push(pathWithFilter); @@ -160,7 +160,7 @@ describe('sync_global_state_with_url', () => { }); test("if data didn't change, kbnUrlStateStorage.set shouldn't be called", () => { - const { stop } = syncGlobalQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); + const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage); filterManager.setFilters([gF, aF]); const spy = jest.spyOn(kbnUrlStateStorage, 'set'); filterManager.setFilters([gF]); // global filters didn't change diff --git a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts similarity index 79% rename from src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts rename to src/plugins/data/public/query/state_sync/sync_state_with_url.ts index 4096cbdfce1c9..ce39a1f29e18f 100644 --- a/src/plugins/data/public/query/state_sync/sync_global_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -18,13 +18,14 @@ */ import { + createStateContainer, IKbnUrlStateStorage, syncState, - createStateContainer, } from '../../../../kibana_utils/public'; import { QuerySetup, QueryStart } from '../query_service'; -import { connectToQueryGlobalState } from './connect_to_global_state'; -import { QueryGlobalState } from './types'; +import { connectToQueryState } from './connect_to_query_state'; +import { QueryState } from './types'; +import { FilterStateStore } from '../../../common/es_query/filters'; const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -33,22 +34,22 @@ const GLOBAL_STATE_STORAGE_KEY = '_g'; * @param QueryService: either setup or start * @param kbnUrlStateStorage to use for syncing */ -export const syncGlobalQueryStateWithUrl = ( - query: Pick, +export const syncQueryStateWithUrl = ( + query: Pick, kbnUrlStateStorage: IKbnUrlStateStorage ) => { const { timefilter: { timefilter }, filterManager, } = query; - const defaultState: QueryGlobalState = { + const defaultState: QueryState = { time: timefilter.getTime(), refreshInterval: timefilter.getRefreshInterval(), filters: filterManager.getGlobalFilters(), }; // retrieve current state from `_g` url - const initialStateFromUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); + const initialStateFromUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); // remember whether there were info in the URL const hasInheritedQueryFromUrl = Boolean( @@ -56,18 +57,22 @@ export const syncGlobalQueryStateWithUrl = ( ); // prepare initial state, whatever was in URL takes precedences over current state in services - const initialState: QueryGlobalState = { + const initialState: QueryState = { ...defaultState, ...initialStateFromUrl, }; const globalQueryStateContainer = createStateContainer(initialState); - const stopSyncingWithStateContainer = connectToQueryGlobalState(query, globalQueryStateContainer); + const stopSyncingWithStateContainer = connectToQueryState(query, globalQueryStateContainer, { + refreshInterval: true, + time: true, + filters: FilterStateStore.GLOBAL_STATE, + }); // if there weren't any initial state in url, // then put _g key into url if (!initialStateFromUrl) { - kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { + kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { replace: true, }); } diff --git a/src/plugins/data/public/query/state_sync/types.ts b/src/plugins/data/public/query/state_sync/types.ts index 7ed6bf6040af2..747d4d45fe29b 100644 --- a/src/plugins/data/public/query/state_sync/types.ts +++ b/src/plugins/data/public/query/state_sync/types.ts @@ -20,17 +20,19 @@ import { Filter, RefreshInterval, TimeRange } from '../../../common'; /** - * State from data services that meant to be preserved between apps + * All query state service state */ -export interface QueryGlobalState { +export interface QueryState { time?: TimeRange; refreshInterval?: RefreshInterval; - filters?: Filter[]; // pinned filters only + filters?: Filter[]; } -/** - * State from data services that should be scope to a single app - */ -export interface QueryAppState { - filters?: Filter[]; // not pinned filters +type QueryStateChangePartial = { + [P in keyof QueryState]?: boolean; +}; + +export interface QueryStateChange extends QueryStateChangePartial { + appFilters?: boolean; // specifies if app filters change + globalFilters?: boolean; // specifies if global filters change } From 3add34896b702bbfde7247bb1d28c55d4791a5db Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 18:41:01 +0100 Subject: [PATCH 22/29] improve --- .../core_plugins/kibana/public/dashboard/plugin.ts | 14 +++++++++++--- .../core_plugins/kibana/public/discover/plugin.ts | 14 +++++++++++--- .../core_plugins/kibana/public/visualize/plugin.ts | 14 +++++++++++--- .../state_sync/create_global_query_observable.ts | 6 +++--- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 28fc4edc548c1..27fe2791440ed 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -18,7 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { App, AppMountParameters, @@ -30,7 +30,11 @@ import { } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RenderDeps } from './np_ready/application'; -import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; +import { + DataPublicPluginStart, + DataPublicPluginSetup, + esFilters, +} from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -90,7 +94,11 @@ export class DashboardPlugin implements Plugin { stateUpdate$: data.query.state$.pipe( filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(esFilters.isFilterPinned), + })) ), }, ], diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index b7a01e1e714d8..3ba0418d35f71 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -18,12 +18,16 @@ */ import { BehaviorSubject } from 'rxjs'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; -import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; +import { + DataPublicPluginStart, + DataPublicPluginSetup, + esFilters, +} from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; @@ -112,7 +116,11 @@ export class DiscoverPlugin implements Plugin { stateUpdate$: plugins.data.query.state$.pipe( filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(esFilters.isFilterPinned), + })) ), }, ], diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 56945971b4f03..dc56a2e02b9ab 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -19,7 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { AppMountParameters, @@ -31,7 +31,11 @@ import { } from 'kibana/public'; import { Storage, createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; +import { + DataPublicPluginStart, + DataPublicPluginSetup, + esFilters, +} from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; @@ -93,7 +97,11 @@ export class VisualizePlugin implements Plugin { stateUpdate$: data.query.state$.pipe( filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(esFilters.isFilterPinned), + })) ), }, ], diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index ed3f4d7473b3d..d0d97bfaaeb36 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -23,7 +23,7 @@ import { TimefilterSetup } from '../timefilter'; import { COMPARE_ALL_OPTIONS, compareFilters, FilterManager } from '../filter_manager'; import { QueryState, QueryStateChange } from './index'; import { createStateContainer } from '../../../../kibana_utils/public'; -import { FilterStateStore } from '../../../common/es_query/filters'; +import { isFilterPinned } from '../../../common/es_query/filters'; export function createQueryStateObservable({ timefilter: { timefilter }, @@ -53,8 +53,8 @@ export function createQueryStateObservable({ currentChange.filters = true; const { filters } = state.get(); - const globalOld = filters?.filter(f => f?.$state?.store === FilterStateStore.GLOBAL_STATE); - const appOld = filters?.filter(f => f?.$state?.store === FilterStateStore.APP_STATE); + const globalOld = filters?.filter(f => isFilterPinned(f)); + const appOld = filters?.filter(f => !isFilterPinned(f)); const globalNew = filterManager.getGlobalFilters(); const appNew = filterManager.getAppFilters(); From d37612297e6477835af62598dbc5c5f76645d96d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 20:05:02 +0100 Subject: [PATCH 23/29] jset -u --- .../query_string_input.test.tsx.snap | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index f176b7d076e5b..4d3add86e5ff0 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -191,14 +191,11 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], @@ -852,14 +849,11 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], @@ -1495,14 +1489,11 @@ exports[`QueryStringInput Should pass the query language to the language switche "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], @@ -2153,14 +2144,11 @@ exports[`QueryStringInput Should pass the query language to the language switche "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], @@ -2796,14 +2784,11 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], @@ -3454,14 +3439,11 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getSuggestions": [MockFunction], "indexPatterns": Object {}, "query": Object { - "app$": Observable { - "_isScalar": false, - }, "filterManager": [MockFunction], - "global$": Observable { + "savedQueries": [MockFunction], + "state$": Observable { "_isScalar": false, }, - "savedQueries": [MockFunction], "timefilter": Object { "history": Object { "add": [MockFunction], From 3f2411447d8c730bfb2c21fcf391537b6c0f8e3c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 20 Feb 2020 12:42:19 +0100 Subject: [PATCH 24/29] @lizozom review --- .../with_data_services/components/app.tsx | 31 +++++++------------ src/plugins/data/public/index.ts | 1 - .../query/state_sync/sync_state_with_url.ts | 2 +- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 952c3e073de78..18424b28abc2c 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -24,11 +24,9 @@ import { Router } from 'react-router-dom'; import { EuiFieldText, - EuiHorizontalRule, EuiPage, EuiPageBody, EuiPageContent, - EuiPageContentHeader, EuiPageHeader, EuiTitle, } from '@elastic/eui'; @@ -41,7 +39,6 @@ import { DataPublicPluginStart, IIndexPattern, QueryState, - QueryStart, Filter, esFilters, } from '../../../../../src/plugins/data/public'; @@ -55,7 +52,6 @@ import { syncState, } from '../../../../../src/plugins/kibana_utils/public'; import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; -// import { Query } from '../../../../../src/plugins/data/common/query/types'; interface StateDemoAppDeps { notifications: CoreStart['notifications']; @@ -69,7 +65,8 @@ interface StateDemoAppDeps { interface AppState { name: string; filters: Filter[]; - // query?: Query; + // TODO: https://github.com/elastic/kibana/issues/58111 + // query?: Query; } const defaultAppState: AppState = { name: '', @@ -95,6 +92,8 @@ const App = ({ useGlobalStateSyncing(data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); + // TODO: https://github.com/elastic/kibana/issues/58111 + // useEffect indefinite cycle inside TopNavManu // const onQuerySubmit = useCallback( // ({ query }) => { // appStateContainer.set({ ...appState, query }); @@ -116,7 +115,7 @@ const App = ({ showSearchBar={true} indexPatterns={[indexPattern]} useDefaultBehaviors={true} - // TODO: would be cool to also get rid of this query syncing + // TODO: https://github.com/elastic/kibana/issues/58111 // onQuerySubmit={onQuerySubmit} // query={appState.query} /> @@ -134,23 +133,12 @@ const App = ({ - - -

- -

-
-
appStateContainer.set({ ...appState, name: e.target.value })} aria-label="My name" /> -
@@ -195,7 +183,10 @@ function useIndexPattern(data: DataPublicPluginStart) { return indexPattern; } -function useGlobalStateSyncing(query: QueryStart, kbnUrlStateStorage: IKbnUrlStateStorage) { +function useGlobalStateSyncing( + query: DataPublicPluginStart['query'], + kbnUrlStateStorage: IKbnUrlStateStorage +) { // setup sync state utils useEffect(() => { // sync global filters, time filters, refresh interval from data.query to url '_g' @@ -208,7 +199,7 @@ function useGlobalStateSyncing(query: QueryStart, kbnUrlStateStorage: IKbnUrlSta function useAppStateSyncing( appStateContainer: BaseStateContainer, - query: QueryStart, + query: DataPublicPluginStart['query'], kbnUrlStateStorage: IKbnUrlStateStorage ) { // setup sync state utils diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 8ff2e25cc2403..b80872b08d86c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -294,7 +294,6 @@ export { connectToQueryState, syncQueryStateWithUrl, QueryState, - QueryStart, getTime, getQueryLog, FilterManager, diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts index ce39a1f29e18f..cd7058b9f8f1c 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -51,7 +51,7 @@ export const syncQueryStateWithUrl = ( // retrieve current state from `_g` url const initialStateFromUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); - // remember whether there were info in the URL + // remember whether there was info in the URL const hasInheritedQueryFromUrl = Boolean( initialStateFromUrl && Object.keys(initialStateFromUrl).length ); From 15bd129f708ff87c4b27692ad53132f7c5724279 Mon Sep 17 00:00:00 2001 From: Liza K Date: Wed, 26 Feb 2020 14:48:53 +0200 Subject: [PATCH 25/29] fix query state in create search bar --- .../with_data_services/components/app.tsx | 28 ++++++----- .../ui/search_bar/create_search_bar.tsx | 46 ++++++++++++------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index 18424b28abc2c..be938e767148c 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { History } from 'history'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; @@ -41,6 +41,7 @@ import { QueryState, Filter, esFilters, + Query, } from '../../../../../src/plugins/data/public'; import { BaseState, @@ -65,8 +66,7 @@ interface StateDemoAppDeps { interface AppState { name: string; filters: Filter[]; - // TODO: https://github.com/elastic/kibana/issues/58111 - // query?: Query; + query?: Query; } const defaultAppState: AppState = { name: '', @@ -92,17 +92,16 @@ const App = ({ useGlobalStateSyncing(data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); - // TODO: https://github.com/elastic/kibana/issues/58111 - // useEffect indefinite cycle inside TopNavManu - // const onQuerySubmit = useCallback( - // ({ query }) => { - // appStateContainer.set({ ...appState, query }); - // }, - // [appStateContainer, appState] - // ); + const onQuerySubmit = useCallback( + ({ query }) => { + appStateContainer.set({ ...appState, query }); + }, + [appStateContainer, appState] + ); const indexPattern = useIndexPattern(data); - if (!indexPattern) return
Loading...
; + if (!indexPattern) + return
No index pattern found. Please create an intex patter before loading...
; // Render the application DOM. // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. @@ -115,9 +114,8 @@ const App = ({ showSearchBar={true} indexPatterns={[indexPattern]} useDefaultBehaviors={true} - // TODO: https://github.com/elastic/kibana/issues/58111 - // onQuerySubmit={onQuerySubmit} - // query={appState.query} + onQuerySubmit={onQuerySubmit} + query={appState.query} /> diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 632385e019e4c..77794ebd4e5a7 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../kibana_react/public'; @@ -117,13 +117,28 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { + const { useDefaultBehaviors } = props; // Handle queries - const [query, setQuery] = useState( - props.query || { - query: '', - language: core.uiSettings.get('search:queryLanguage'), + const queryRef = useRef(props.query); + const onQuerySubmitRef = useRef(props.onQuerySubmit); + const defaultQuery = { + query: '', + language: core.uiSettings.get('search:queryLanguage'), + }; + const [query, setQuery] = useState(defaultQuery); + + useEffect(() => { + if (props.query !== queryRef.current) { + queryRef.current = props.query; + setQuery(props.query || defaultQuery); } - ); + }, [defaultQuery, props.query]); + + useEffect(() => { + if (props.onQuerySubmit !== onQuerySubmitRef.current) { + onQuerySubmitRef.current = props.onQuerySubmit; + } + }, [props.onQuerySubmit]); // handle service state updates. // i.e. filters being added from a visualization directly to filterManager. @@ -150,16 +165,15 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) // Fire onQuerySubmit on query or timerange change useEffect(() => { - if (!props.useDefaultBehaviors) return; - if (props.onQuerySubmit) - props.onQuerySubmit( - { - dateRange: timeRange, - query, - }, - true - ); - }, [props, props.onQuerySubmit, props.useDefaultBehaviors, query, timeRange]); + if (!useDefaultBehaviors || !onQuerySubmitRef.current) return; + onQuerySubmitRef.current( + { + dateRange: timeRange, + query, + }, + true + ); + }, [query, timeRange, useDefaultBehaviors]); return ( Date: Wed, 26 Feb 2020 14:57:17 +0200 Subject: [PATCH 26/29] removed useCallback --- src/plugins/data/public/ui/search_bar/create_search_bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 77794ebd4e5a7..2b7565eb83e46 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../kibana_react/public'; From 83e16c0fb3f44be1f63bc622ee0d6fda906a149d Mon Sep 17 00:00:00 2001 From: Liza K Date: Wed, 26 Feb 2020 15:18:09 +0200 Subject: [PATCH 27/29] Enabled saved queries --- .../public/with_data_services/components/app.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index be938e767148c..c820929d8a61d 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -116,6 +116,7 @@ const App = ({ useDefaultBehaviors={true} onQuerySubmit={onQuerySubmit} query={appState.query} + showSaveQuery={true} /> From e3513e02a744a9335f302c6921c030fccdbe455d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 27 Feb 2020 06:45:49 +0100 Subject: [PATCH 28/29] fix setting saved search --- src/plugins/data/public/ui/search_bar/create_search_bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 2b7565eb83e46..7d65e947c0f04 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -125,7 +125,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) query: '', language: core.uiSettings.get('search:queryLanguage'), }; - const [query, setQuery] = useState(defaultQuery); + const [query, setQuery] = useState(props.query || defaultQuery); useEffect(() => { if (props.query !== queryRef.current) { From cc91afba1414de6de93777e52557c9c8eb23606e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 27 Feb 2020 10:05:12 +0100 Subject: [PATCH 29/29] jest -u --- .../renderers/zeek/__snapshots__/zeek_details.test.tsx.snap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index 6b866aeecc831..9117737847dda 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -956,6 +956,9 @@ tr:hover .c3:focus::before { "query": Object { "filterManager": [MockFunction], "savedQueries": [MockFunction], + "state$": Observable { + "_isScalar": false, + }, "timefilter": Object { "history": Object { "add": [MockFunction], @@ -974,8 +977,10 @@ tr:hover .c3:focus::before { "getEnabledUpdated$": [MockFunction], "getFetch$": [MockFunction], "getRefreshInterval": [MockFunction], + "getRefreshIntervalDefaults": [MockFunction], "getRefreshIntervalUpdate$": [MockFunction], "getTime": [MockFunction], + "getTimeDefaults": [MockFunction], "getTimeUpdate$": [MockFunction], "isAutoRefreshSelectorEnabled": [MockFunction], "isTimeRangeSelectorEnabled": [MockFunction],