mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Search Sessions] Improve session restoration back button (#87635)
This commit is contained in:
parent
9c410b81ca
commit
07b210e42d
27 changed files with 560 additions and 148 deletions
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md)
|
||||
|
||||
## IKbnUrlStateStorage.cancel property
|
||||
|
||||
cancels any pending url updates
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
cancel: () => void;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md)
|
||||
|
||||
## IKbnUrlStateStorage.flush property
|
||||
|
||||
Synchronously runs any pending url updates, returned boolean indicates if change occurred.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
flush: (opts?: {
|
||||
replace?: boolean;
|
||||
}) => boolean;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [kbnUrlControls](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md)
|
||||
|
||||
## IKbnUrlStateStorage.kbnUrlControls property
|
||||
|
||||
Lower level wrapper around history library that handles batching multiple URL updates into one history change
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
kbnUrlControls: IKbnUrlControls;
|
||||
```
|
|
@ -20,9 +20,8 @@ export interface IKbnUrlStateStorage extends IStateStorage
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | <code>() => void</code> | cancels any pending url updates |
|
||||
| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <code><State = unknown>(key: string) => Observable<State | null></code> | |
|
||||
| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | <code>(opts?: {</code><br/><code> replace?: boolean;</code><br/><code> }) => boolean</code> | Synchronously runs any pending url updates, returned boolean indicates if change occurred. |
|
||||
| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <code><State = unknown>(key: string) => State | null</code> | |
|
||||
| [kbnUrlControls](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.kbnurlcontrols.md) | <code>IKbnUrlControls</code> | Lower level wrapper around history library that handles batching multiple URL updates into one history change |
|
||||
| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <code><State>(key: string, state: State, opts?: {</code><br/><code> replace: boolean;</code><br/><code> }) => Promise<string | undefined></code> | |
|
||||
|
||||
|
|
|
@ -6,22 +6,22 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { History } from 'history';
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { merge, Subject, Subscription } from 'rxjs';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { debounceTime, tap } from 'rxjs/operators';
|
||||
import { useKibana } from '../../../kibana_react/public';
|
||||
import { DashboardConstants } from '../dashboard_constants';
|
||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
||||
import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from './types';
|
||||
import {
|
||||
getChangesFromAppStateForContainerState,
|
||||
getDashboardContainerInput,
|
||||
getFiltersSubscription,
|
||||
getInputSubscription,
|
||||
getOutputSubscription,
|
||||
getFiltersSubscription,
|
||||
getSearchSessionIdFromURL,
|
||||
getDashboardContainerInput,
|
||||
getChangesFromAppStateForContainerState,
|
||||
} from './dashboard_app_functions';
|
||||
import {
|
||||
useDashboardBreadcrumbs,
|
||||
|
@ -30,11 +30,11 @@ import {
|
|||
useSavedDashboard,
|
||||
} from './hooks';
|
||||
|
||||
import { removeQueryParam } from '../services/kibana_utils';
|
||||
import { IndexPattern } from '../services/data';
|
||||
import { EmbeddableRenderer } from '../services/embeddable';
|
||||
import { DashboardContainerInput } from '.';
|
||||
import { leaveConfirmStrings } from '../dashboard_strings';
|
||||
import { createQueryParamObservable, replaceUrlHashQuery } from '../../../kibana_utils/public';
|
||||
|
||||
export interface DashboardAppProps {
|
||||
history: History;
|
||||
|
@ -59,7 +59,7 @@ export function DashboardApp({
|
|||
indexPatterns: indexPatternService,
|
||||
} = useKibana<DashboardAppServices>().services;
|
||||
|
||||
const [lastReloadTime, setLastReloadTime] = useState(0);
|
||||
const triggerRefresh$ = useMemo(() => new Subject<{ force?: boolean }>(), []);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]);
|
||||
|
||||
const savedDashboard = useSavedDashboard(savedDashboardId, history);
|
||||
|
@ -68,9 +68,13 @@ export function DashboardApp({
|
|||
history
|
||||
);
|
||||
const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false);
|
||||
const searchSessionIdQuery$ = useMemo(
|
||||
() => createQueryParamObservable(history, DashboardConstants.SEARCH_SESSION_ID),
|
||||
[history]
|
||||
);
|
||||
|
||||
const refreshDashboardContainer = useCallback(
|
||||
(lastReloadRequestTime?: number) => {
|
||||
(force?: boolean) => {
|
||||
if (!dashboardContainer || !dashboardStateManager) {
|
||||
return;
|
||||
}
|
||||
|
@ -80,7 +84,7 @@ export function DashboardApp({
|
|||
appStateDashboardInput: getDashboardContainerInput({
|
||||
isEmbeddedExternally: Boolean(embedSettings),
|
||||
dashboardStateManager,
|
||||
lastReloadRequestTime,
|
||||
lastReloadRequestTime: force ? Date.now() : undefined,
|
||||
dashboardCapabilities,
|
||||
query: data.query,
|
||||
}),
|
||||
|
@ -100,10 +104,35 @@ export function DashboardApp({
|
|||
const shouldRefetch = Object.keys(changes).some(
|
||||
(changeKey) => !noRefetchKeys.includes(changeKey as keyof DashboardContainerInput)
|
||||
);
|
||||
if (getSearchSessionIdFromURL(history)) {
|
||||
// going away from a background search results
|
||||
removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true);
|
||||
}
|
||||
|
||||
const newSearchSessionId: string | undefined = (() => {
|
||||
// do not update session id if this is irrelevant state change to prevent excessive searches
|
||||
if (!shouldRefetch) return;
|
||||
|
||||
let searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||
if (searchSessionIdFromURL) {
|
||||
if (
|
||||
data.search.session.isRestore() &&
|
||||
data.search.session.isCurrentSession(searchSessionIdFromURL)
|
||||
) {
|
||||
// navigating away from a restored session
|
||||
dashboardStateManager.kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => {
|
||||
if (nextUrl.includes(DashboardConstants.SEARCH_SESSION_ID)) {
|
||||
return replaceUrlHashQuery(nextUrl, (query) => {
|
||||
delete query[DashboardConstants.SEARCH_SESSION_ID];
|
||||
return query;
|
||||
});
|
||||
}
|
||||
return nextUrl;
|
||||
});
|
||||
searchSessionIdFromURL = undefined;
|
||||
} else {
|
||||
data.search.session.restore(searchSessionIdFromURL);
|
||||
}
|
||||
}
|
||||
|
||||
return searchSessionIdFromURL ?? data.search.session.start();
|
||||
})();
|
||||
|
||||
if (changes.viewMode) {
|
||||
setViewMode(changes.viewMode);
|
||||
|
@ -111,8 +140,7 @@ export function DashboardApp({
|
|||
|
||||
dashboardContainer.updateInput({
|
||||
...changes,
|
||||
// do not start a new session if this is irrelevant state change to prevent excessive searches
|
||||
...(shouldRefetch && { searchSessionId: data.search.session.start() }),
|
||||
...(newSearchSessionId && { searchSessionId: newSearchSessionId }),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -159,23 +187,42 @@ export function DashboardApp({
|
|||
subscriptions.add(
|
||||
merge(
|
||||
...[timeFilter.getRefreshIntervalUpdate$(), timeFilter.getTimeUpdate$()]
|
||||
).subscribe(() => refreshDashboardContainer())
|
||||
).subscribe(() => triggerRefresh$.next())
|
||||
);
|
||||
|
||||
subscriptions.add(
|
||||
merge(
|
||||
data.search.session.onRefresh$,
|
||||
data.query.timefilter.timefilter.getAutoRefreshFetch$()
|
||||
data.query.timefilter.timefilter.getAutoRefreshFetch$(),
|
||||
searchSessionIdQuery$
|
||||
).subscribe(() => {
|
||||
setLastReloadTime(() => new Date().getTime());
|
||||
triggerRefresh$.next({ force: true });
|
||||
})
|
||||
);
|
||||
|
||||
dashboardStateManager.registerChangeListener(() => {
|
||||
// we aren't checking dirty state because there are changes the container needs to know about
|
||||
// that won't make the dashboard "dirty" - like a view mode change.
|
||||
refreshDashboardContainer();
|
||||
triggerRefresh$.next();
|
||||
});
|
||||
|
||||
// debounce `refreshDashboardContainer()`
|
||||
// use `forceRefresh=true` in case at least one debounced trigger asked for it
|
||||
let forceRefresh: boolean = false;
|
||||
subscriptions.add(
|
||||
triggerRefresh$
|
||||
.pipe(
|
||||
tap((trigger) => {
|
||||
forceRefresh = forceRefresh || (trigger?.force ?? false);
|
||||
}),
|
||||
debounceTime(50)
|
||||
)
|
||||
.subscribe(() => {
|
||||
refreshDashboardContainer(forceRefresh);
|
||||
forceRefresh = false;
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscriptions.unsubscribe();
|
||||
};
|
||||
|
@ -187,6 +234,8 @@ export function DashboardApp({
|
|||
data.search.session,
|
||||
indexPatternService,
|
||||
dashboardStateManager,
|
||||
searchSessionIdQuery$,
|
||||
triggerRefresh$,
|
||||
refreshDashboardContainer,
|
||||
]);
|
||||
|
||||
|
@ -216,11 +265,6 @@ export function DashboardApp({
|
|||
};
|
||||
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
|
||||
|
||||
// Refresh the dashboard container when lastReloadTime changes
|
||||
useEffect(() => {
|
||||
refreshDashboardContainer(lastReloadTime);
|
||||
}, [lastReloadTime, refreshDashboardContainer]);
|
||||
|
||||
return (
|
||||
<div className="app-container dshAppContainer">
|
||||
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
|
||||
|
@ -242,7 +286,7 @@ export function DashboardApp({
|
|||
// The user can still request a reload in the query bar, even if the
|
||||
// query is the same, and in that case, we have to explicitly ask for
|
||||
// a reload, since no state changes will cause it.
|
||||
setLastReloadTime(() => new Date().getTime());
|
||||
triggerRefresh$.next({ force: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -72,7 +72,7 @@ export class DashboardStateManager {
|
|||
>;
|
||||
private readonly stateContainerChangeSub: Subscription;
|
||||
private readonly STATE_STORAGE_KEY = '_a';
|
||||
private readonly kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
public readonly kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
private readonly stateSyncRef: ISyncStateRef;
|
||||
private readonly history: History;
|
||||
private readonly usageCollection: UsageCollectionSetup | undefined;
|
||||
|
@ -596,7 +596,7 @@ export class DashboardStateManager {
|
|||
this.toUrlState(this.stateContainer.get())
|
||||
);
|
||||
// immediately forces scheduled updates and changes location
|
||||
return this.kbnUrlStateStorage.flush({ replace });
|
||||
return !!this.kbnUrlStateStorage.kbnUrlControls.flush(replace);
|
||||
}
|
||||
|
||||
// TODO: find nicer solution for this
|
||||
|
|
|
@ -4,10 +4,16 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
|
|||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -144,10 +150,16 @@ exports[`after fetch hideWriteControls 1`] = `
|
|||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -237,10 +249,16 @@ exports[`after fetch initialFilter 1`] = `
|
|||
initialFilter="testFilter"
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -376,10 +394,16 @@ exports[`after fetch renders all table rows 1`] = `
|
|||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -515,10 +539,16 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
|||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -654,10 +684,16 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
|||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"flush": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('sync_query_state_with_url', () => {
|
|||
test('url is actually changed when data in services changes', () => {
|
||||
const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage);
|
||||
filterManager.setFilters([gF, aF]);
|
||||
kbnUrlStateStorage.flush(); // sync force location change
|
||||
kbnUrlStateStorage.kbnUrlControls.flush(); // sync force location change
|
||||
expect(history.location.hash).toMatchInlineSnapshot(
|
||||
`"#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!t,index:'logstash-*',key:query,negate:!t,type:custom,value:'%7B%22match%22:%7B%22key1%22:%22value1%22%7D%7D'),query:(match:(key1:value1)))),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"`
|
||||
);
|
||||
|
@ -126,7 +126,7 @@ describe('sync_query_state_with_url', () => {
|
|||
|
||||
test('when url is changed, filters synced back to filterManager', () => {
|
||||
const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage);
|
||||
kbnUrlStateStorage.cancel(); // stop initial syncing pending update
|
||||
kbnUrlStateStorage.kbnUrlControls.cancel(); // stop initial syncing pending update
|
||||
history.push(pathWithFilter);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
stop();
|
||||
|
|
|
@ -206,7 +206,7 @@ export function getState({
|
|||
}
|
||||
},
|
||||
// helper function just needed for testing
|
||||
flushToUrl: (replace?: boolean) => stateStorage.flush({ replace }),
|
||||
flushToUrl: (replace?: boolean) => stateStorage.kbnUrlControls.flush(replace),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -47,8 +47,6 @@ import { popularizeField } from '../helpers/popularize_field';
|
|||
import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state';
|
||||
import { addFatalError } from '../../../../kibana_legacy/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
|
||||
import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public';
|
||||
import {
|
||||
DEFAULT_COLUMNS_SETTING,
|
||||
MODIFY_COLUMNS_ON_SWITCH,
|
||||
|
@ -62,6 +60,7 @@ import { getTopNavLinks } from '../components/top_nav/get_top_nav_links';
|
|||
import { updateSearchSource } from '../helpers/update_search_source';
|
||||
import { calcFieldCounts } from '../helpers/calc_field_counts';
|
||||
import { getDefaultSort } from './doc_table/lib/get_default_sort';
|
||||
import { DiscoverSearchSessionManager } from './discover_search_session';
|
||||
|
||||
const services = getServices();
|
||||
|
||||
|
@ -86,9 +85,6 @@ const fetchStatuses = {
|
|||
ERROR: 'error',
|
||||
};
|
||||
|
||||
const getSearchSessionIdFromURL = (history) =>
|
||||
getQueryParams(history.location)[SEARCH_SESSION_ID_QUERY_PARAM];
|
||||
|
||||
const app = getAngularModule();
|
||||
|
||||
app.config(($routeProvider) => {
|
||||
|
@ -177,7 +173,9 @@ function discoverController($route, $scope, Promise) {
|
|||
const { isDefault: isDefaultType } = indexPatternsUtils;
|
||||
const subscriptions = new Subscription();
|
||||
const refetch$ = new Subject();
|
||||
|
||||
let inspectorRequest;
|
||||
let isChangingIndexPattern = false;
|
||||
const savedSearch = $route.current.locals.savedObjects.savedSearch;
|
||||
$scope.searchSource = savedSearch.searchSource;
|
||||
$scope.indexPattern = resolveIndexPattern(
|
||||
|
@ -195,15 +193,10 @@ function discoverController($route, $scope, Promise) {
|
|||
};
|
||||
|
||||
const history = getHistory();
|
||||
// used for restoring a search session
|
||||
let isInitialSearch = true;
|
||||
|
||||
// search session requested a data refresh
|
||||
subscriptions.add(
|
||||
data.search.session.onRefresh$.subscribe(() => {
|
||||
refetch$.next();
|
||||
})
|
||||
);
|
||||
const searchSessionManager = new DiscoverSearchSessionManager({
|
||||
history,
|
||||
session: data.search.session,
|
||||
});
|
||||
|
||||
const state = getState({
|
||||
getStateDefaults,
|
||||
|
@ -255,6 +248,7 @@ function discoverController($route, $scope, Promise) {
|
|||
$scope.$evalAsync(async () => {
|
||||
if (oldStatePartial.index !== newStatePartial.index) {
|
||||
//in case of index pattern switch the route has currently to be reloaded, legacy
|
||||
isChangingIndexPattern = true;
|
||||
$route.reload();
|
||||
return;
|
||||
}
|
||||
|
@ -351,7 +345,12 @@ function discoverController($route, $scope, Promise) {
|
|||
if (abortController) abortController.abort();
|
||||
savedSearch.destroy();
|
||||
subscriptions.unsubscribe();
|
||||
data.search.session.clear();
|
||||
if (!isChangingIndexPattern) {
|
||||
// HACK:
|
||||
// do not clear session when changing index pattern due to how state management around it is setup
|
||||
// it will be cleared by searchSessionManager on controller reload instead
|
||||
data.search.session.clear();
|
||||
}
|
||||
appStateUnsubscribe();
|
||||
stopStateSync();
|
||||
stopSyncingGlobalStateWithUrl();
|
||||
|
@ -475,7 +474,8 @@ function discoverController($route, $scope, Promise) {
|
|||
return (
|
||||
config.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
|
||||
savedSearch.id !== undefined ||
|
||||
timefilter.getRefreshInterval().pause === false
|
||||
timefilter.getRefreshInterval().pause === false ||
|
||||
searchSessionManager.hasSearchSessionIdInURL()
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -486,7 +486,8 @@ function discoverController($route, $scope, Promise) {
|
|||
filterManager.getFetches$(),
|
||||
timefilter.getFetch$(),
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
data.query.queryString.getUpdates$()
|
||||
data.query.queryString.getUpdates$(),
|
||||
searchSessionManager.newSearchSessionIdFromURL$
|
||||
).pipe(debounceTime(100));
|
||||
|
||||
subscriptions.add(
|
||||
|
@ -512,6 +513,13 @@ function discoverController($route, $scope, Promise) {
|
|||
)
|
||||
);
|
||||
|
||||
subscriptions.add(
|
||||
data.search.session.onRefresh$.subscribe(() => {
|
||||
searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
|
||||
refetch$.next();
|
||||
})
|
||||
);
|
||||
|
||||
$scope.changeInterval = (interval) => {
|
||||
if (interval) {
|
||||
setAppState({ interval });
|
||||
|
@ -591,20 +599,7 @@ function discoverController($route, $scope, Promise) {
|
|||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
|
||||
const searchSessionId = (() => {
|
||||
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||
if (searchSessionIdFromURL) {
|
||||
if (isInitialSearch) {
|
||||
data.search.session.restore(searchSessionIdFromURL);
|
||||
isInitialSearch = false;
|
||||
return searchSessionIdFromURL;
|
||||
} else {
|
||||
// navigating away from background search
|
||||
removeQueryParam(history, SEARCH_SESSION_ID_QUERY_PARAM);
|
||||
}
|
||||
}
|
||||
return data.search.session.start();
|
||||
})();
|
||||
const searchSessionId = searchSessionManager.getNextSearchSessionId();
|
||||
|
||||
$scope
|
||||
.updateDataSource()
|
||||
|
@ -631,6 +626,7 @@ function discoverController($route, $scope, Promise) {
|
|||
|
||||
$scope.handleRefresh = function (_payload, isUpdate) {
|
||||
if (isUpdate === false) {
|
||||
searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
|
||||
refetch$.next();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DiscoverSearchSessionManager } from './discover_search_session';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { dataPluginMock } from '../../../../data/public/mocks';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
|
||||
describe('DiscoverSearchSessionManager', () => {
|
||||
const history = createMemoryHistory();
|
||||
const session = dataPluginMock.createStartContract().search.session as jest.Mocked<
|
||||
DataPublicPluginStart['search']['session']
|
||||
>;
|
||||
const searchSessionManager = new DiscoverSearchSessionManager({
|
||||
history,
|
||||
session,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
history.push('/');
|
||||
session.start.mockReset();
|
||||
session.restore.mockReset();
|
||||
session.getSessionId.mockReset();
|
||||
session.isCurrentSession.mockReset();
|
||||
session.isRestore.mockReset();
|
||||
});
|
||||
|
||||
describe('getNextSearchSessionId', () => {
|
||||
test('starts a new session', () => {
|
||||
const nextId = 'id';
|
||||
session.start.mockImplementationOnce(() => nextId);
|
||||
|
||||
const id = searchSessionManager.getNextSearchSessionId();
|
||||
expect(id).toEqual(nextId);
|
||||
expect(session.start).toBeCalled();
|
||||
});
|
||||
|
||||
test('restores a session using query param from the URL', () => {
|
||||
const nextId = 'id_from_url';
|
||||
history.push(`/?searchSessionId=${nextId}`);
|
||||
|
||||
const id = searchSessionManager.getNextSearchSessionId();
|
||||
expect(id).toEqual(nextId);
|
||||
expect(session.restore).toBeCalled();
|
||||
});
|
||||
|
||||
test('removes query param from the URL when navigating away from a restored session', () => {
|
||||
const idFromUrl = 'id_from_url';
|
||||
history.push(`/?searchSessionId=${idFromUrl}`);
|
||||
|
||||
const nextId = 'id';
|
||||
session.start.mockImplementationOnce(() => nextId);
|
||||
session.isCurrentSession.mockImplementationOnce(() => true);
|
||||
session.isRestore.mockImplementationOnce(() => true);
|
||||
|
||||
const id = searchSessionManager.getNextSearchSessionId();
|
||||
expect(id).toEqual(nextId);
|
||||
expect(session.start).toBeCalled();
|
||||
expect(history.location.search).toMatchInlineSnapshot(`""`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newSearchSessionIdFromURL$', () => {
|
||||
test('notifies about searchSessionId changes in the URL', () => {
|
||||
const emits: Array<string | null> = [];
|
||||
|
||||
const sub = searchSessionManager.newSearchSessionIdFromURL$.subscribe((newId) => {
|
||||
emits.push(newId);
|
||||
});
|
||||
|
||||
history.push(`/?searchSessionId=id1`);
|
||||
history.push(`/?searchSessionId=id1`);
|
||||
session.isCurrentSession.mockImplementationOnce(() => true);
|
||||
history.replace(`/?searchSessionId=id2`); // should skip current this
|
||||
history.replace(`/`);
|
||||
history.push(`/?searchSessionId=id1`);
|
||||
history.push(`/`);
|
||||
|
||||
expect(emits).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"id1",
|
||||
null,
|
||||
"id1",
|
||||
null,
|
||||
]
|
||||
`);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
import {
|
||||
createQueryParamObservable,
|
||||
getQueryParams,
|
||||
removeQueryParam,
|
||||
} from '../../../../kibana_utils/public';
|
||||
import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
|
||||
|
||||
export interface DiscoverSearchSessionManagerDeps {
|
||||
history: History;
|
||||
session: DataPublicPluginStart['search']['session'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps with state management of search session and {@link SEARCH_SESSION_ID_QUERY_PARAM} in the URL
|
||||
*/
|
||||
export class DiscoverSearchSessionManager {
|
||||
/**
|
||||
* Notifies about `searchSessionId` changes in the URL,
|
||||
* skips if `searchSessionId` matches current search session id
|
||||
*/
|
||||
readonly newSearchSessionIdFromURL$ = createQueryParamObservable<string>(
|
||||
this.deps.history,
|
||||
SEARCH_SESSION_ID_QUERY_PARAM
|
||||
).pipe(
|
||||
filter((searchSessionId) => {
|
||||
if (!searchSessionId) return true;
|
||||
return !this.deps.session.isCurrentSession(searchSessionId);
|
||||
})
|
||||
);
|
||||
|
||||
constructor(private readonly deps: DiscoverSearchSessionManagerDeps) {}
|
||||
|
||||
/**
|
||||
* Get next session id by either starting or restoring a session.
|
||||
* When navigating away from the restored session {@link SEARCH_SESSION_ID_QUERY_PARAM} is removed from the URL using history.replace
|
||||
*/
|
||||
getNextSearchSessionId() {
|
||||
let searchSessionIdFromURL = this.getSearchSessionIdFromURL();
|
||||
if (searchSessionIdFromURL) {
|
||||
if (
|
||||
this.deps.session.isRestore() &&
|
||||
this.deps.session.isCurrentSession(searchSessionIdFromURL)
|
||||
) {
|
||||
// navigating away from a restored session
|
||||
this.removeSearchSessionIdFromURL({ replace: true });
|
||||
searchSessionIdFromURL = undefined;
|
||||
} else {
|
||||
this.deps.session.restore(searchSessionIdFromURL);
|
||||
}
|
||||
}
|
||||
|
||||
return searchSessionIdFromURL ?? this.deps.session.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Discovers {@link SEARCH_SESSION_ID_QUERY_PARAM} from the URL
|
||||
* @param replace - methods to change the URL
|
||||
*/
|
||||
removeSearchSessionIdFromURL({ replace = true }: { replace?: boolean } = { replace: true }) {
|
||||
if (this.hasSearchSessionIdInURL()) {
|
||||
removeQueryParam(this.deps.history, SEARCH_SESSION_ID_QUERY_PARAM, replace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a {@link SEARCH_SESSION_ID_QUERY_PARAM} currently in the URL
|
||||
*/
|
||||
hasSearchSessionIdInURL(): boolean {
|
||||
return !!this.getSearchSessionIdFromURL();
|
||||
}
|
||||
|
||||
private getSearchSessionIdFromURL = () =>
|
||||
getQueryParams(this.deps.history.location)[SEARCH_SESSION_ID_QUERY_PARAM] as string | undefined;
|
||||
}
|
|
@ -200,7 +200,7 @@ export function getState({
|
|||
setState(appStateContainerModified, defaultState);
|
||||
},
|
||||
getPreviousAppState: () => previousAppState,
|
||||
flushToUrl: () => stateStorage.flush(),
|
||||
flushToUrl: () => stateStorage.kbnUrlControls.flush(),
|
||||
isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -96,11 +96,11 @@ setTimeout(() => {
|
|||
}, 0);
|
||||
```
|
||||
|
||||
For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` provides these advanced apis:
|
||||
For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` exposes `kbnUrlStateStorage.kbnUrlControls` that exposes these advanced apis:
|
||||
|
||||
- `kbnUrlStateStorage.flush({replace: boolean})` - allows to synchronously apply any pending updates.
|
||||
`replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened
|
||||
- `kbnUrlStateStorage.cancel()` - cancels any pending updates
|
||||
- `kbnUrlStateStorage.kbnUrlControls.flush({replace: boolean})` - allows to synchronously apply any pending updates.
|
||||
`replace` option allows using `history.replace()` instead of `history.push()`.
|
||||
- `kbnUrlStateStorage.kbnUrlControls.cancel()` - cancels any pending updates.
|
||||
|
||||
### Sharing one `kbnUrlStateStorage` instance
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
createHistoryObservable,
|
||||
createQueryParamObservable,
|
||||
createQueryParamsObservable,
|
||||
} from './history_observable';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { ParsedQuery } from 'query-string';
|
||||
|
||||
let history: History;
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
});
|
||||
|
||||
test('createHistoryObservable', () => {
|
||||
const obs$ = createHistoryObservable(history);
|
||||
const emits: string[] = [];
|
||||
obs$.subscribe(({ location }) => {
|
||||
emits.push(location.pathname + location.search);
|
||||
});
|
||||
|
||||
history.push('/test');
|
||||
history.push('/');
|
||||
|
||||
expect(emits.length).toEqual(2);
|
||||
expect(emits).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/test",
|
||||
"/",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('createQueryParamsObservable', () => {
|
||||
const obs$ = createQueryParamsObservable(history);
|
||||
const emits: ParsedQuery[] = [];
|
||||
obs$.subscribe((params) => {
|
||||
emits.push(params);
|
||||
});
|
||||
|
||||
history.push('/test');
|
||||
history.push('/test?foo=bar');
|
||||
history.push('/?foo=bar');
|
||||
history.push('/test?foo=bar&foo1=bar1');
|
||||
|
||||
expect(emits.length).toEqual(2);
|
||||
expect(emits).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"foo": "bar",
|
||||
},
|
||||
Object {
|
||||
"foo": "bar",
|
||||
"foo1": "bar1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('createQueryParamObservable', () => {
|
||||
const obs$ = createQueryParamObservable(history, 'foo');
|
||||
const emits: unknown[] = [];
|
||||
obs$.subscribe((param) => {
|
||||
emits.push(param);
|
||||
});
|
||||
|
||||
history.push('/test');
|
||||
history.push('/test?foo=bar');
|
||||
history.push('/?foo=bar');
|
||||
history.push('/test?foo=baaaar&foo1=bar1');
|
||||
history.push('/test?foo1=bar1');
|
||||
|
||||
expect(emits.length).toEqual(3);
|
||||
expect(emits).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"bar",
|
||||
"baaaar",
|
||||
null,
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Action, History, Location } from 'history';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ParsedQuery } from 'query-string';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getQueryParams } from './get_query_params';
|
||||
import { distinctUntilChangedWithInitialValue } from '../../common';
|
||||
|
||||
/**
|
||||
* Convert history.listen into an observable
|
||||
* @param history - {@link History} instance
|
||||
*/
|
||||
export function createHistoryObservable(
|
||||
history: History
|
||||
): Observable<{ location: Location; action: Action }> {
|
||||
return new Observable((observer) => {
|
||||
const unlisten = history.listen((location, action) => observer.next({ location, action }));
|
||||
return () => {
|
||||
unlisten();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable that emits every time any of query params change.
|
||||
* Uses deepEqual check.
|
||||
* @param history - {@link History} instance
|
||||
*/
|
||||
export function createQueryParamsObservable(history: History): Observable<ParsedQuery> {
|
||||
return createHistoryObservable(history).pipe(
|
||||
map(({ location }) => ({ ...getQueryParams(location) })),
|
||||
distinctUntilChangedWithInitialValue({ ...getQueryParams(history.location) }, deepEqual)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable that emits every time _paramKey_ changes
|
||||
* @param history - {@link History} instance
|
||||
* @param paramKey - query param key to observe
|
||||
*/
|
||||
export function createQueryParamObservable<Param = unknown>(
|
||||
history: History,
|
||||
paramKey: string
|
||||
): Observable<Param | null> {
|
||||
return createQueryParamsObservable(history).pipe(
|
||||
map((params) => (params[paramKey] ?? null) as Param | null),
|
||||
distinctUntilChangedWithInitialValue(
|
||||
(getQueryParams(history.location)[paramKey] ?? null) as Param | null,
|
||||
deepEqual
|
||||
)
|
||||
);
|
||||
}
|
|
@ -9,3 +9,4 @@
|
|||
export { removeQueryParam } from './remove_query_param';
|
||||
export { redirectWhenMissing } from './redirect_when_missing';
|
||||
export { getQueryParams } from './get_query_params';
|
||||
export * from './history_observable';
|
||||
|
|
|
@ -68,7 +68,14 @@ export {
|
|||
StopSyncStateFnType,
|
||||
} from './state_sync';
|
||||
export { Configurable, CollectConfigProps } from './ui';
|
||||
export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history';
|
||||
export {
|
||||
removeQueryParam,
|
||||
redirectWhenMissing,
|
||||
getQueryParams,
|
||||
createQueryParamsObservable,
|
||||
createHistoryObservable,
|
||||
createQueryParamObservable,
|
||||
} from './history';
|
||||
export { applyDiff } from './state_management/utils/diff_object';
|
||||
export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter';
|
||||
|
||||
|
|
|
@ -22,14 +22,12 @@ export const createSessionStorageStateStorage: (storage?: Storage) => ISessionSt
|
|||
|
||||
// @public
|
||||
export interface IKbnUrlStateStorage extends IStateStorage {
|
||||
cancel: () => void;
|
||||
// (undocumented)
|
||||
change$: <State = unknown>(key: string) => Observable<State | null>;
|
||||
flush: (opts?: {
|
||||
replace?: boolean;
|
||||
}) => boolean;
|
||||
// (undocumented)
|
||||
get: <State = unknown>(key: string) => State | null;
|
||||
// Warning: (ae-forgotten-export) The symbol "IKbnUrlControls" needs to be exported by the entry point index.d.ts
|
||||
kbnUrlControls: IKbnUrlControls;
|
||||
// (undocumented)
|
||||
set: <State>(key: string, state: State, opts?: {
|
||||
replace: boolean;
|
||||
|
|
|
@ -255,7 +255,7 @@ describe('state_sync', () => {
|
|||
expect(history.length).toBe(startHistoryLength);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
|
||||
urlSyncStrategy.flush();
|
||||
urlSyncStrategy.kbnUrlControls.flush();
|
||||
|
||||
expect(history.length).toBe(startHistoryLength + 1);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
|
@ -290,7 +290,7 @@ describe('state_sync', () => {
|
|||
expect(history.length).toBe(startHistoryLength);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
|
||||
urlSyncStrategy.cancel();
|
||||
urlSyncStrategy.kbnUrlControls.cancel();
|
||||
|
||||
expect(history.length).toBe(startHistoryLength);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
|
|
|
@ -39,11 +39,11 @@ describe('KbnUrlStateStorage', () => {
|
|||
const key = '_s';
|
||||
urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
expect(urlStateStorage.flush()).toBe(true);
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(state);
|
||||
|
||||
expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update
|
||||
});
|
||||
|
||||
it('should cancel url updates', async () => {
|
||||
|
@ -51,7 +51,7 @@ describe('KbnUrlStateStorage', () => {
|
|||
const key = '_s';
|
||||
const pr = urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
urlStateStorage.cancel();
|
||||
urlStateStorage.kbnUrlControls.cancel();
|
||||
await pr;
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(null);
|
||||
|
@ -215,11 +215,11 @@ describe('KbnUrlStateStorage', () => {
|
|||
const key = '_s';
|
||||
urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
|
||||
expect(urlStateStorage.flush()).toBe(true);
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(state);
|
||||
|
||||
expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update
|
||||
expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update
|
||||
});
|
||||
|
||||
it('should cancel url updates', async () => {
|
||||
|
@ -227,7 +227,7 @@ describe('KbnUrlStateStorage', () => {
|
|||
const key = '_s';
|
||||
const pr = urlStateStorage.set(key, state);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
|
||||
urlStateStorage.cancel();
|
||||
urlStateStorage.kbnUrlControls.cancel();
|
||||
await pr;
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
|
||||
expect(urlStateStorage.get(key)).toEqual(null);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { IStateStorage } from './types';
|
|||
import {
|
||||
createKbnUrlControls,
|
||||
getStateFromKbnUrl,
|
||||
IKbnUrlControls,
|
||||
setStateToKbnUrl,
|
||||
} from '../../state_management/url';
|
||||
|
||||
|
@ -39,16 +40,9 @@ export interface IKbnUrlStateStorage extends IStateStorage {
|
|||
change$: <State = unknown>(key: string) => Observable<State | null>;
|
||||
|
||||
/**
|
||||
* cancels any pending url updates
|
||||
* Lower level wrapper around history library that handles batching multiple URL updates into one history change
|
||||
*/
|
||||
cancel: () => void;
|
||||
|
||||
/**
|
||||
* Synchronously runs any pending url updates, returned boolean indicates if change occurred.
|
||||
* @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update
|
||||
* @returns boolean - indicates if there was an update to flush
|
||||
*/
|
||||
flush: (opts?: { replace?: boolean }) => boolean;
|
||||
kbnUrlControls: IKbnUrlControls;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,11 +108,6 @@ export const createKbnUrlStateStorage = (
|
|||
}),
|
||||
share()
|
||||
),
|
||||
flush: ({ replace = false }: { replace?: boolean } = {}) => {
|
||||
return !!url.flush(replace);
|
||||
},
|
||||
cancel() {
|
||||
url.cancel();
|
||||
},
|
||||
kbnUrlControls: url,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -227,7 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
describe('usage of discover:searchOnPageLoad', () => {
|
||||
it('should fetch data from ES initially when discover:searchOnPageLoad is false', async function () {
|
||||
it('should not fetch data from ES initially when discover:searchOnPageLoad is false', async function () {
|
||||
await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': false });
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.header.awaitKibanaChrome();
|
||||
|
@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.discover.getNrOfFetches()).to.be(0);
|
||||
});
|
||||
|
||||
it('should not fetch data from ES initially when discover:searchOnPageLoad is true', async function () {
|
||||
it('should fetch data from ES initially when discover:searchOnPageLoad is true', async function () {
|
||||
await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': true });
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.header.awaitKibanaChrome();
|
||||
|
|
|
@ -42,10 +42,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const sessionIds = await getSessionIds();
|
||||
|
||||
// Discover calls destroy on index pattern change, which explicitly closes a session
|
||||
expect(sessionIds.length).to.be(2);
|
||||
expect(sessionIds[0].length).to.be(0);
|
||||
expect(sessionIds[1].length).not.to.be(0);
|
||||
expect(sessionIds.length).to.be(1);
|
||||
});
|
||||
|
||||
it('Starts on a refresh', async () => {
|
||||
|
|
|
@ -30,6 +30,6 @@ export function updateGlobalState(newState: MapsGlobalState, flushUrlState = fal
|
|||
...newState,
|
||||
});
|
||||
if (flushUrlState) {
|
||||
kbnUrlStateStorage.flush({ replace: true });
|
||||
kbnUrlStateStorage.kbnUrlControls.flush(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await searchSessions.deleteAllSearchSessions();
|
||||
});
|
||||
|
||||
it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => {
|
||||
it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes. Back button restores a session.', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard('Not Delayed');
|
||||
const url = await browser.getCurrentUrl();
|
||||
let url = await browser.getCurrentUrl();
|
||||
const fakeSessionId = '__fake__';
|
||||
const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`;
|
||||
await browser.get(savedSessionURL);
|
||||
|
@ -53,6 +53,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
'Sum of Bytes by Extension'
|
||||
);
|
||||
expect(session2).not.to.be(fakeSessionId);
|
||||
|
||||
// back button should restore the session:
|
||||
url = await browser.getCurrentUrl();
|
||||
expect(url).not.to.contain('searchSessionId');
|
||||
|
||||
await browser.goBack();
|
||||
|
||||
url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain('searchSessionId');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
expect(
|
||||
await dashboardPanelActions.getSearchSessionIdByTitle('Sum of Bytes by Extension')
|
||||
).to.be(fakeSessionId);
|
||||
});
|
||||
|
||||
it('Saves and restores a session', async () => {
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const browser = getService('browser');
|
||||
const inspector = getService('inspector');
|
||||
const PageObjects = getPageObjects(['discover', 'common', 'timePicker', 'header']);
|
||||
const searchSessions = getService('searchSessions');
|
||||
|
||||
describe('discover async search', () => {
|
||||
before(async () => {
|
||||
|
@ -31,18 +32,33 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
expect(searchSessionId2).not.to.be(searchSessionId1);
|
||||
});
|
||||
|
||||
it('search session id should be picked up from the URL, non existing session id errors out', async () => {
|
||||
const url = await browser.getCurrentUrl();
|
||||
it('search session id should be picked up from the URL, non existing session id errors out, back button restores a session', async () => {
|
||||
let url = await browser.getCurrentUrl();
|
||||
const fakeSearchSessionId = '__test__';
|
||||
const savedSessionURL = url + `&searchSessionId=${fakeSearchSessionId}`;
|
||||
await browser.navigateTo(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
await testSubjects.existOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId
|
||||
const searchSessionId1 = await getSearchSessionId();
|
||||
expect(searchSessionId1).to.be(fakeSearchSessionId);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('completed');
|
||||
const searchSessionId2 = await getSearchSessionId();
|
||||
expect(searchSessionId2).not.to.be(searchSessionId1);
|
||||
|
||||
// back button should restore the session:
|
||||
url = await browser.getCurrentUrl();
|
||||
expect(url).not.to.contain('searchSessionId');
|
||||
|
||||
await browser.goBack();
|
||||
|
||||
url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain('searchSessionId');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
expect(await getSearchSessionId()).to.be(fakeSearchSessionId);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue