mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover] Keep fetched results when switching tabs (#216741)
## Summary This PR implements the initial work to keep fetched results when switching tabs: - Avoid cancelling the current document request when switching tabs (this still needs more work, especially migrating the `use_esql_mode` hook to the central data fetching). - Move `DiscoverStateContainer` and `DiscoverCustomizationService` to `RuntimeStateManager` so they can be reused by tabs without reinitializing. - Re-add the current tab ID to `InternalStateStore` for high-level tab management only (called `unsafeCurrentId` now to discourage misuse). - Move `initializeAndSync` and initial `fetchData` call to the `initializeSession` thunk to avoid calling it when switching back to existing tabs. - Move URL tracking directly into `DiscoverSavedSearchContainer` since it previously used a hook which could now become out of sync because `initializeAndSync` was moved (URL could update before the hook was called). - Support fully disconnecting tabs with a new `disconnectTab` thunk (called on tab close and for all remaining tabs when leaving Discover). - Sync global services to current tab state when switching tabs (this should probably be cleaned up more, but it should work as a start). - Basic implementation of `getPreviewData` (needs to be cleaned up). - A couple of small misc changes (e.g. fixing scrollbar when tabs are enabled). Part of #216475. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
990432ccc0
commit
c3bcdab741
27 changed files with 637 additions and 579 deletions
|
@ -18,7 +18,7 @@ import {
|
|||
createInternalStateStore,
|
||||
createRuntimeStateManager,
|
||||
} from '../application/main/state_management/redux';
|
||||
import type { HistoryLocationState } from '../build_services';
|
||||
import type { DiscoverServices, HistoryLocationState } from '../build_services';
|
||||
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { History } from 'history';
|
||||
|
@ -31,6 +31,7 @@ export function getDiscoverStateMock({
|
|||
runtimeStateManager,
|
||||
history,
|
||||
customizationContext = mockCustomizationContext,
|
||||
services: originalServices = discoverServiceMock,
|
||||
}: {
|
||||
isTimeBased?: boolean;
|
||||
savedSearch?: SavedSearch | false;
|
||||
|
@ -38,12 +39,13 @@ export function getDiscoverStateMock({
|
|||
stateStorageContainer?: IKbnUrlStateStorage;
|
||||
history?: History<HistoryLocationState>;
|
||||
customizationContext?: DiscoverCustomizationContext;
|
||||
services?: DiscoverServices;
|
||||
}) {
|
||||
if (!history) {
|
||||
history = createBrowserHistory<HistoryLocationState>();
|
||||
history.push('/');
|
||||
}
|
||||
const services = { ...discoverServiceMock, history };
|
||||
const services = { ...originalServices, history };
|
||||
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
|
||||
const toasts = services.core.notifications.toasts;
|
||||
stateStorageContainer =
|
||||
|
@ -62,7 +64,7 @@ export function getDiscoverStateMock({
|
|||
urlStateStorage: stateStorageContainer,
|
||||
});
|
||||
const container = getDiscoverStateContainer({
|
||||
tabId: internalState.getState().tabs.allIds[0],
|
||||
tabId: internalState.getState().tabs.unsafeCurrentId,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer,
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
RuntimeStateProvider,
|
||||
internalStateActions,
|
||||
} from '../../state_management/redux';
|
||||
import { TABS_ENABLED } from '../../discover_main_route';
|
||||
|
||||
function getStateContainer(savedSearch?: SavedSearch) {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
|
||||
|
@ -177,10 +178,12 @@ const mountComponent = async ({
|
|||
|
||||
describe('Discover histogram layout component', () => {
|
||||
describe('render', () => {
|
||||
it('should render null if there is no search session', async () => {
|
||||
const { component } = await mountComponent({ searchSessionId: null });
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
if (!TABS_ENABLED) {
|
||||
it('should render null if there is no search session', async () => {
|
||||
const { component } = await mountComponent({ searchSessionId: null });
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
it('should not render null if there is a search session', async () => {
|
||||
const { component } = await mountComponent();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useDiscoverHistogram } from './use_discover_histogram';
|
|||
import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content';
|
||||
import { useAppStateSelector } from '../../state_management/discover_app_state_container';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
import { TABS_ENABLED } from '../../discover_main_route';
|
||||
|
||||
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
|
||||
container: HTMLElement | null;
|
||||
|
@ -51,8 +52,11 @@ export const DiscoverHistogramLayout = ({
|
|||
|
||||
// Initialized when the first search has been requested or
|
||||
// when in ES|QL mode since search sessions are not supported
|
||||
if (!searchSessionId && !isEsqlMode) {
|
||||
return null;
|
||||
// TODO: Handle this for tabs
|
||||
if (!TABS_ENABLED) {
|
||||
if (!searchSessionId && !isEsqlMode) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -58,6 +58,7 @@ import { PanelsToggle } from '../../../../components/panels_toggle';
|
|||
import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux';
|
||||
import { TABS_ENABLED } from '../../discover_main_route';
|
||||
|
||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
|
@ -368,7 +369,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
background-color: ${pageBackgroundColor};
|
||||
|
||||
${useEuiBreakpoint(['m', 'l', 'xl'])} {
|
||||
${kibanaFullBodyHeightCss()}
|
||||
${kibanaFullBodyHeightCss(TABS_ENABLED ? 32 : undefined)}
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
|
||||
export { BrandedLoadingIndicator } from './branded_loading_indicator';
|
||||
export { NoDataPage } from './no_data_page';
|
||||
export {
|
||||
DiscoverSessionView,
|
||||
type DiscoverSessionViewProps,
|
||||
type DiscoverSessionViewRef,
|
||||
} from './session_view';
|
||||
export { DiscoverSessionView, type DiscoverSessionViewProps } from './session_view';
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import React, { useEffect } from 'react';
|
||||
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
|
||||
import { useUrlTracking } from '../../hooks/use_url_tracking';
|
||||
import type { DiscoverStateContainer } from '../../state_management/discover_state';
|
||||
import { DiscoverLayout } from '../layout';
|
||||
import { setBreadcrumbs } from '../../../../utils/breadcrumbs';
|
||||
|
@ -19,7 +18,6 @@ import { useSavedSearchAliasMatchRedirect } from '../../../../hooks/saved_search
|
|||
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
|
||||
import { useAdHocDataViews } from '../../hooks/use_adhoc_data_views';
|
||||
import { useEsqlMode } from '../../hooks/use_esql_mode';
|
||||
import { addLog } from '../../../../utils/add_log';
|
||||
|
||||
const DiscoverLayoutMemoized = React.memo(DiscoverLayout);
|
||||
|
||||
|
@ -30,18 +28,15 @@ export interface DiscoverMainProps {
|
|||
stateContainer: DiscoverStateContainer;
|
||||
}
|
||||
|
||||
export function DiscoverMainApp(props: DiscoverMainProps) {
|
||||
const { stateContainer } = props;
|
||||
export function DiscoverMainApp({ stateContainer }: DiscoverMainProps) {
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const services = useDiscoverServices();
|
||||
const { chrome, docLinks, data, spaces, history } = services;
|
||||
|
||||
useUrlTracking(stateContainer);
|
||||
|
||||
/**
|
||||
* Adhoc data views functionality
|
||||
*/
|
||||
useAdHocDataViews({ stateContainer, services });
|
||||
useAdHocDataViews();
|
||||
|
||||
/**
|
||||
* State changes (data view, columns), when a text base query result is returned
|
||||
|
@ -51,16 +46,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
stateContainer,
|
||||
});
|
||||
|
||||
/**
|
||||
* Start state syncing and fetch data if necessary
|
||||
*/
|
||||
useEffect(() => {
|
||||
stateContainer.actions.initializeAndSync();
|
||||
addLog('[DiscoverMainApp] state container initialization triggers data fetching');
|
||||
stateContainer.actions.fetchData(true);
|
||||
return () => stateContainer.actions.stopSyncing();
|
||||
}, [stateContainer]);
|
||||
|
||||
/**
|
||||
* SavedSearch dependent initializing
|
||||
*/
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import React from 'react';
|
||||
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useLatest from 'react-use/lib/useLatest';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||
import { useUrl } from '../../hooks/use_url';
|
||||
import { useAlertResultsToast } from '../../hooks/use_alert_results_toast';
|
||||
import { createDataViewDataSource } from '../../../../../common/data_sources';
|
||||
|
@ -21,7 +23,6 @@ import type { MainHistoryLocationState } from '../../../../../common';
|
|||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import type { DiscoverAppState } from '../../state_management/discover_app_state_container';
|
||||
import { getDiscoverStateContainer } from '../../state_management/discover_state';
|
||||
import type { DiscoverStateContainer } from '../../state_management/discover_state';
|
||||
import {
|
||||
RuntimeStateProvider,
|
||||
internalStateActions,
|
||||
|
@ -39,7 +40,7 @@ import type {
|
|||
import type { InternalStateStore, RuntimeStateManager } from '../../state_management/redux';
|
||||
import {
|
||||
DiscoverCustomizationProvider,
|
||||
useDiscoverCustomizationService,
|
||||
getConnectedCustomizationService,
|
||||
} from '../../../../customizations';
|
||||
import { DiscoverError } from '../../../../components/common/error_alert';
|
||||
import { NoDataPage } from './no_data_page';
|
||||
|
@ -57,171 +58,163 @@ export interface DiscoverSessionViewProps {
|
|||
runtimeStateManager: RuntimeStateManager;
|
||||
}
|
||||
|
||||
export interface DiscoverSessionViewRef {
|
||||
stopSyncing: () => void;
|
||||
interface SessionInitializationState {
|
||||
showNoDataPage: boolean;
|
||||
}
|
||||
|
||||
type SessionInitializationState =
|
||||
| {
|
||||
showNoDataPage: true;
|
||||
stateContainer: undefined;
|
||||
}
|
||||
| {
|
||||
showNoDataPage: false;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
};
|
||||
|
||||
type InitializeSession = (options?: {
|
||||
dataViewSpec?: DataViewSpec | undefined;
|
||||
defaultUrlState?: DiscoverAppState;
|
||||
}) => Promise<SessionInitializationState>;
|
||||
|
||||
export const DiscoverSessionView = forwardRef<DiscoverSessionViewRef, DiscoverSessionViewProps>(
|
||||
(
|
||||
{
|
||||
customizationContext,
|
||||
customizationCallbacks,
|
||||
urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const dispatch = useInternalStateDispatch();
|
||||
const services = useDiscoverServices();
|
||||
const { core, history, getScopedHistory } = services;
|
||||
const { id: discoverSessionId } = useParams<{ id?: string }>();
|
||||
const currentTabId = useCurrentTabSelector((tab) => tab.id);
|
||||
const initializeSessionAction = useCurrentTabAction(internalStateActions.initializeSession);
|
||||
const [initializeSessionState, initializeSession] = useAsyncFunction<InitializeSession>(
|
||||
async ({ dataViewSpec, defaultUrlState } = {}) => {
|
||||
initializeSessionState.value?.stateContainer?.actions.stopSyncing();
|
||||
|
||||
const stateContainer = getDiscoverStateContainer({
|
||||
tabId: currentTabId,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer: urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
});
|
||||
const { showNoDataPage } = await dispatch(
|
||||
initializeSessionAction({
|
||||
initializeSessionParams: {
|
||||
stateContainer,
|
||||
discoverSessionId,
|
||||
dataViewSpec,
|
||||
defaultUrlState,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return showNoDataPage ? { showNoDataPage } : { showNoDataPage, stateContainer };
|
||||
}
|
||||
);
|
||||
const initializeSessionWithDefaultLocationState = useLatest(() => {
|
||||
const historyLocationState = getScopedHistory<
|
||||
MainHistoryLocationState & { defaultState?: DiscoverAppState }
|
||||
>()?.location.state;
|
||||
initializeSession({
|
||||
dataViewSpec: historyLocationState?.dataViewSpec,
|
||||
defaultUrlState: historyLocationState?.defaultState,
|
||||
export const DiscoverSessionView = ({
|
||||
customizationContext,
|
||||
customizationCallbacks,
|
||||
urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
}: DiscoverSessionViewProps) => {
|
||||
const dispatch = useInternalStateDispatch();
|
||||
const services = useDiscoverServices();
|
||||
const { core, history, getScopedHistory } = services;
|
||||
const { id: discoverSessionId } = useParams<{ id?: string }>();
|
||||
const currentTabId = useCurrentTabSelector((tab) => tab.id);
|
||||
const currentStateContainer = useCurrentTabRuntimeState(
|
||||
runtimeStateManager,
|
||||
(tab) => tab.stateContainer$
|
||||
);
|
||||
const currentCustomizationService = useCurrentTabRuntimeState(
|
||||
runtimeStateManager,
|
||||
(tab) => tab.customizationService$
|
||||
);
|
||||
const initializeSessionAction = useCurrentTabAction(internalStateActions.initializeSession);
|
||||
const [initializeSessionState, initializeSession] = useAsyncFunction<InitializeSession>(
|
||||
async ({ dataViewSpec, defaultUrlState } = {}) => {
|
||||
const stateContainer = getDiscoverStateContainer({
|
||||
tabId: currentTabId,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer: urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
});
|
||||
const customizationService = await getConnectedCustomizationService({
|
||||
stateContainer,
|
||||
customizationCallbacks,
|
||||
});
|
||||
});
|
||||
const customizationService = useDiscoverCustomizationService({
|
||||
customizationCallbacks,
|
||||
stateContainer: initializeSessionState.value?.stateContainer,
|
||||
});
|
||||
const initializationState = useInternalStateSelector((state) => state.initializationState);
|
||||
const currentDataView = useCurrentTabRuntimeState(
|
||||
runtimeStateManager,
|
||||
(tab) => tab.currentDataView$
|
||||
);
|
||||
const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
stopSyncing: () => initializeSessionState.value?.stateContainer?.actions.stopSyncing(),
|
||||
}),
|
||||
[initializeSessionState.value?.stateContainer]
|
||||
);
|
||||
return dispatch(
|
||||
initializeSessionAction({
|
||||
initializeSessionParams: {
|
||||
stateContainer,
|
||||
customizationService,
|
||||
discoverSessionId,
|
||||
dataViewSpec,
|
||||
defaultUrlState,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
currentStateContainer && currentCustomizationService
|
||||
? { loading: false, value: { showNoDataPage: false } }
|
||||
: { loading: true }
|
||||
);
|
||||
const initializeSessionWithDefaultLocationState = useLatest(() => {
|
||||
const historyLocationState = getScopedHistory<
|
||||
MainHistoryLocationState & { defaultState?: DiscoverAppState }
|
||||
>()?.location.state;
|
||||
initializeSession({
|
||||
dataViewSpec: historyLocationState?.dataViewSpec,
|
||||
defaultUrlState: historyLocationState?.defaultState,
|
||||
});
|
||||
});
|
||||
const initializationState = useInternalStateSelector((state) => state.initializationState);
|
||||
const currentDataView = useCurrentTabRuntimeState(
|
||||
runtimeStateManager,
|
||||
(tab) => tab.currentDataView$
|
||||
);
|
||||
const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
|
||||
|
||||
useEffect(() => {
|
||||
useMount(() => {
|
||||
if (!currentStateContainer || !currentCustomizationService) {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
}, [discoverSessionId, initializeSessionWithDefaultLocationState]);
|
||||
|
||||
useUrl({
|
||||
history,
|
||||
savedSearchId: discoverSessionId,
|
||||
onNewUrl: () => {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
},
|
||||
});
|
||||
|
||||
useAlertResultsToast();
|
||||
|
||||
useExecutionContext(core.executionContext, {
|
||||
type: 'application',
|
||||
page: 'app',
|
||||
id: discoverSessionId || 'new',
|
||||
});
|
||||
|
||||
if (initializeSessionState.loading) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
});
|
||||
|
||||
if (initializeSessionState.error) {
|
||||
if (initializeSessionState.error instanceof SavedObjectNotFound) {
|
||||
return (
|
||||
<RedirectWhenSavedObjectNotFound
|
||||
error={initializeSessionState.error}
|
||||
discoverSessionId={discoverSessionId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
useUpdateEffect(() => {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
}, [discoverSessionId, initializeSessionWithDefaultLocationState]);
|
||||
|
||||
return <DiscoverError error={initializeSessionState.error} />;
|
||||
}
|
||||
useUrl({
|
||||
history,
|
||||
savedSearchId: discoverSessionId,
|
||||
onNewUrl: () => {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
},
|
||||
});
|
||||
|
||||
if (initializeSessionState.value.showNoDataPage) {
|
||||
useAlertResultsToast();
|
||||
|
||||
useExecutionContext(core.executionContext, {
|
||||
type: 'application',
|
||||
page: 'app',
|
||||
id: discoverSessionId || 'new',
|
||||
});
|
||||
|
||||
if (initializeSessionState.loading) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (initializeSessionState.error) {
|
||||
if (initializeSessionState.error instanceof SavedObjectNotFound) {
|
||||
return (
|
||||
<NoDataPage
|
||||
{...initializationState}
|
||||
onDataViewCreated={async (dataViewUnknown) => {
|
||||
await dispatch(internalStateActions.loadDataViewList());
|
||||
dispatch(
|
||||
internalStateActions.setInitializationState({
|
||||
hasESData: true,
|
||||
hasUserDataView: true,
|
||||
})
|
||||
);
|
||||
const dataView = dataViewUnknown as DataView;
|
||||
initializeSession({
|
||||
defaultUrlState: dataView.id
|
||||
? { dataSource: createDataViewDataSource({ dataViewId: dataView.id }) }
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
onESQLNavigationComplete={() => {
|
||||
initializeSession();
|
||||
}}
|
||||
<RedirectWhenSavedObjectNotFound
|
||||
error={initializeSessionState.error}
|
||||
discoverSessionId={discoverSessionId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customizationService || !currentDataView) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
return <DiscoverError error={initializeSessionState.error} />;
|
||||
}
|
||||
|
||||
if (initializeSessionState.value.showNoDataPage) {
|
||||
return (
|
||||
<DiscoverCustomizationProvider value={customizationService}>
|
||||
<DiscoverMainProvider value={initializeSessionState.value.stateContainer}>
|
||||
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
|
||||
<DiscoverMainApp stateContainer={initializeSessionState.value.stateContainer} />
|
||||
</RuntimeStateProvider>
|
||||
</DiscoverMainProvider>
|
||||
</DiscoverCustomizationProvider>
|
||||
<NoDataPage
|
||||
{...initializationState}
|
||||
onDataViewCreated={async (dataViewUnknown) => {
|
||||
await dispatch(internalStateActions.loadDataViewList());
|
||||
dispatch(
|
||||
internalStateActions.setInitializationState({
|
||||
hasESData: true,
|
||||
hasUserDataView: true,
|
||||
})
|
||||
);
|
||||
const dataView = dataViewUnknown as DataView;
|
||||
initializeSession({
|
||||
defaultUrlState: dataView.id
|
||||
? { dataSource: createDataViewDataSource({ dataViewId: dataView.id }) }
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
onESQLNavigationComplete={() => {
|
||||
initializeSession();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (!currentStateContainer || !currentCustomizationService || !currentDataView) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscoverCustomizationProvider value={currentCustomizationService}>
|
||||
<DiscoverMainProvider value={currentStateContainer}>
|
||||
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
|
||||
<DiscoverMainApp stateContainer={currentStateContainer} />
|
||||
</RuntimeStateProvider>
|
||||
</DiscoverMainProvider>
|
||||
</DiscoverCustomizationProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,56 +8,68 @@
|
|||
*/
|
||||
|
||||
import { type TabItem, UnifiedTabs, TabStatus } from '@kbn/unified-tabs';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import type { DiscoverSessionViewRef } from '../session_view';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view';
|
||||
import {
|
||||
CurrentTabProvider,
|
||||
createTabItem,
|
||||
internalStateActions,
|
||||
selectAllTabs,
|
||||
selectTabRuntimeState,
|
||||
useInternalStateDispatch,
|
||||
useInternalStateSelector,
|
||||
} from '../../state_management/redux';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
|
||||
export const TabsView = ({
|
||||
initialTabId,
|
||||
sessionViewProps,
|
||||
}: {
|
||||
initialTabId: string;
|
||||
sessionViewProps: DiscoverSessionViewProps;
|
||||
}) => {
|
||||
export const TabsView = (props: DiscoverSessionViewProps) => {
|
||||
const services = useDiscoverServices();
|
||||
const dispatch = useInternalStateDispatch();
|
||||
const allTabs = useInternalStateSelector(selectAllTabs);
|
||||
const [currentTabId, setCurrentTabId] = useState(initialTabId);
|
||||
const currentTabId = useInternalStateSelector((state) => state.tabs.unsafeCurrentId);
|
||||
const [initialItems] = useState<TabItem[]>(() => allTabs.map((tab) => pick(tab, 'id', 'label')));
|
||||
const sessionViewRef = useRef<DiscoverSessionViewRef>(null);
|
||||
|
||||
return (
|
||||
<UnifiedTabs
|
||||
services={services}
|
||||
initialItems={initialItems}
|
||||
onChanged={async (updateState) => {
|
||||
await dispatch(
|
||||
internalStateActions.updateTabs({
|
||||
currentTabId,
|
||||
updateState,
|
||||
stopSyncing: sessionViewRef.current?.stopSyncing,
|
||||
})
|
||||
);
|
||||
setCurrentTabId(updateState.selectedItem?.id ?? currentTabId);
|
||||
}}
|
||||
onChanged={(updateState) => dispatch(internalStateActions.updateTabs(updateState))}
|
||||
createItem={() => createTabItem(allTabs)}
|
||||
getPreviewData={() => ({
|
||||
query: { language: 'kuery', query: 'sample query' },
|
||||
status: TabStatus.SUCCESS,
|
||||
})}
|
||||
getPreviewData={(item) => {
|
||||
const defaultQuery = { language: 'kuery', query: '(Empty query)' };
|
||||
const stateContainer = selectTabRuntimeState(
|
||||
props.runtimeStateManager,
|
||||
item.id
|
||||
).stateContainer$.getValue();
|
||||
|
||||
if (!stateContainer) {
|
||||
return {
|
||||
query: defaultQuery,
|
||||
status: TabStatus.RUNNING,
|
||||
};
|
||||
}
|
||||
|
||||
const fetchStatus = stateContainer.dataState.data$.main$.getValue().fetchStatus;
|
||||
const query = stateContainer.appState.getState().query;
|
||||
|
||||
return {
|
||||
query: isOfAggregateQueryType(query)
|
||||
? { esql: query.esql.trim() || defaultQuery.query }
|
||||
: query
|
||||
? { ...query, query: query.query.trim() || defaultQuery.query }
|
||||
: defaultQuery,
|
||||
status: [FetchStatus.UNINITIALIZED, FetchStatus.COMPLETE].includes(fetchStatus)
|
||||
? TabStatus.SUCCESS
|
||||
: fetchStatus === FetchStatus.ERROR
|
||||
? TabStatus.ERROR
|
||||
: TabStatus.RUNNING,
|
||||
};
|
||||
}}
|
||||
renderContent={() => (
|
||||
<CurrentTabProvider currentTabId={currentTabId}>
|
||||
<DiscoverSessionView key={currentTabId} ref={sessionViewRef} {...sessionViewProps} />
|
||||
<DiscoverSessionView key={currentTabId} {...props} />
|
||||
</CurrentTabProvider>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -25,13 +25,13 @@ import type { MainHistoryLocationState } from '../../../common';
|
|||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import type { RootProfileState } from '../../context_awareness';
|
||||
|
||||
let mockCustomizationService: DiscoverCustomizationService | undefined;
|
||||
let mockCustomizationService: Promise<DiscoverCustomizationService> | undefined;
|
||||
|
||||
jest.mock('../../customizations', () => {
|
||||
const originalModule = jest.requireActual('../../customizations');
|
||||
return {
|
||||
...originalModule,
|
||||
useDiscoverCustomizationService: () => mockCustomizationService,
|
||||
useDiscoverCustomizationService: () => () => mockCustomizationService,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -58,7 +58,7 @@ jest.mock('../../context_awareness', () => {
|
|||
|
||||
describe('DiscoverMainRoute', () => {
|
||||
beforeEach(() => {
|
||||
mockCustomizationService = createCustomizationService();
|
||||
mockCustomizationService = Promise.resolve(createCustomizationService());
|
||||
mockRootProfileState = defaultRootProfileState;
|
||||
});
|
||||
|
||||
|
@ -124,13 +124,16 @@ describe('DiscoverMainRoute', () => {
|
|||
});
|
||||
|
||||
test('renders LoadingIndicator while customizations are loading', async () => {
|
||||
mockCustomizationService = undefined;
|
||||
let resolveService = (_: DiscoverCustomizationService) => {};
|
||||
mockCustomizationService = new Promise((resolve) => {
|
||||
resolveService = resolve;
|
||||
});
|
||||
const component = mountComponent(true, true);
|
||||
await waitFor(() => {
|
||||
component.update();
|
||||
expect(component.find(DiscoverMainApp).exists()).toBe(false);
|
||||
});
|
||||
mockCustomizationService = createCustomizationService();
|
||||
resolveService(createCustomizationService());
|
||||
await waitFor(() => {
|
||||
component.setProps({}).update();
|
||||
expect(component.find(DiscoverMainApp).exists()).toBe(true);
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
|||
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import type { CustomizationCallback, DiscoverCustomizationContext } from '../../customizations';
|
||||
import {
|
||||
|
@ -35,7 +36,7 @@ import { useAsyncFunction } from './hooks/use_async_function';
|
|||
import { TabsView } from './components/tabs_view';
|
||||
|
||||
// TEMPORARY: This is a temporary flag to enable/disable tabs in Discover until the feature is fully implemented.
|
||||
const TABS_ENABLED = false;
|
||||
export const TABS_ENABLED = false;
|
||||
|
||||
export interface MainRouteProps {
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
|
@ -76,7 +77,6 @@ export const DiscoverMainRoute = ({
|
|||
urlStateStorage,
|
||||
})
|
||||
);
|
||||
const [initialTabId] = useState(() => internalState.getState().tabs.allIds[0]);
|
||||
const { initializeProfileDataViews } = useDefaultAdHocDataViews({ internalState });
|
||||
const [mainRouteInitializationState, initializeMainRoute] = useAsyncFunction<InitializeMainRoute>(
|
||||
async (loadedRootProfileState) => {
|
||||
|
@ -105,6 +105,12 @@ export const DiscoverMainRoute = ({
|
|||
}
|
||||
}, [initializeMainRoute, rootProfileState]);
|
||||
|
||||
useUnmount(() => {
|
||||
for (const tabId of Object.keys(runtimeStateManager.tabs.byId)) {
|
||||
internalState.dispatch(internalStateActions.disconnectTab({ tabId }));
|
||||
}
|
||||
});
|
||||
|
||||
if (rootProfileState.rootProfileLoading || mainRouteInitializationState.loading) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
|
@ -139,9 +145,9 @@ export const DiscoverMainRoute = ({
|
|||
<InternalStateProvider store={internalState}>
|
||||
<rootProfileState.AppWrapper>
|
||||
{TABS_ENABLED ? (
|
||||
<TabsView initialTabId={initialTabId} sessionViewProps={sessionViewProps} />
|
||||
<TabsView {...sessionViewProps} />
|
||||
) : (
|
||||
<CurrentTabProvider currentTabId={initialTabId}>
|
||||
<CurrentTabProvider currentTabId={internalState.getState().tabs.unsafeCurrentId}>
|
||||
<DiscoverSessionView {...sessionViewProps} />
|
||||
</CurrentTabProvider>
|
||||
)}
|
||||
|
|
|
@ -9,30 +9,24 @@
|
|||
|
||||
import { useEffect } from 'react';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
import { useSavedSearch } from '../state_management/discover_state_provider';
|
||||
import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../../../constants';
|
||||
import type { DiscoverStateContainer } from '../state_management/discover_state';
|
||||
import { useFiltersValidation } from './use_filters_validation';
|
||||
import { useIsEsqlMode } from './use_is_esql_mode';
|
||||
import { useCurrentDataView } from '../state_management/redux';
|
||||
import { useDiscoverServices } from '../../../hooks/use_discover_services';
|
||||
|
||||
export const useAdHocDataViews = ({
|
||||
services,
|
||||
}: {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
services: DiscoverServices;
|
||||
}) => {
|
||||
export const useAdHocDataViews = () => {
|
||||
const dataView = useCurrentDataView();
|
||||
const savedSearch = useSavedSearch();
|
||||
const isEsqlMode = useIsEsqlMode();
|
||||
const { filterManager, toastNotifications } = services;
|
||||
const { filterManager, toastNotifications, trackUiMetric } = useDiscoverServices();
|
||||
|
||||
useEffect(() => {
|
||||
if (dataView && !dataView.isPersisted()) {
|
||||
services.trackUiMetric?.(METRIC_TYPE.COUNT, ADHOC_DATA_VIEW_RENDER_EVENT);
|
||||
trackUiMetric?.(METRIC_TYPE.COUNT, ADHOC_DATA_VIEW_RENDER_EVENT);
|
||||
}
|
||||
}, [dataView, isEsqlMode, services]);
|
||||
}, [dataView, isEsqlMode, trackUiMetric]);
|
||||
|
||||
/**
|
||||
* Takes care of checking data view id references in filters
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useUrlTracking } from './use_url_tracking';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { createDiscoverServicesMock } from '../../../__mocks__/services';
|
||||
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
import type { DiscoverStateContainer } from '../state_management/discover_state';
|
||||
import { omit } from 'lodash';
|
||||
import { createSavedSearchAdHocMock, createSavedSearchMock } from '../../../__mocks__/saved_search';
|
||||
import { internalStateActions } from '../state_management/redux';
|
||||
|
||||
const renderUrlTracking = ({
|
||||
services,
|
||||
stateContainer,
|
||||
}: {
|
||||
services: DiscoverServices;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}) =>
|
||||
renderHook(useUrlTracking, {
|
||||
initialProps: stateContainer,
|
||||
wrapper: ({ children }) => (
|
||||
<KibanaContextProvider services={services}>{children}</KibanaContextProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe('useUrlTracking', () => {
|
||||
it('should enable URL tracking for a persisted data view', () => {
|
||||
const services = createDiscoverServicesMock();
|
||||
const savedSearch = omit(createSavedSearchMock(), 'id');
|
||||
const stateContainer = getDiscoverStateMock({ savedSearch });
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
renderUrlTracking({ services, stateContainer });
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should disable URL tracking for an ad hoc data view', () => {
|
||||
const services = createDiscoverServicesMock();
|
||||
const savedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
const stateContainer = getDiscoverStateMock({ savedSearch });
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
renderUrlTracking({ services, stateContainer });
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should enable URL tracking if the ad hoc data view is a default profile data view', () => {
|
||||
const services = createDiscoverServicesMock();
|
||||
const savedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
const stateContainer = getDiscoverStateMock({ savedSearch });
|
||||
stateContainer.internalState.dispatch(
|
||||
internalStateActions.setDefaultProfileAdHocDataViews([
|
||||
savedSearch.searchSource.getField('index')!,
|
||||
])
|
||||
);
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
renderUrlTracking({ services, stateContainer });
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should enable URL tracking with an ad hoc data view if in ES|QL mode', () => {
|
||||
const services = createDiscoverServicesMock();
|
||||
const savedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
savedSearch.searchSource.setField('query', { esql: 'FROM test' });
|
||||
const stateContainer = getDiscoverStateMock({ savedSearch });
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
renderUrlTracking({ services, stateContainer });
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should enable URL tracking with an ad hoc data view if the saved search has an ID (persisted)', () => {
|
||||
const services = createDiscoverServicesMock();
|
||||
const savedSearch = createSavedSearchAdHocMock();
|
||||
const stateContainer = getDiscoverStateMock({ savedSearch });
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
renderUrlTracking({ services, stateContainer });
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { useDiscoverServices } from '../../../hooks/use_discover_services';
|
||||
import type { DiscoverStateContainer } from '../state_management/discover_state';
|
||||
|
||||
/**
|
||||
* Enable/disable kbn url tracking (That's the URL used when selecting Discover in the side menu)
|
||||
*/
|
||||
export function useUrlTracking(stateContainer: DiscoverStateContainer) {
|
||||
const { savedSearchState, internalState } = stateContainer;
|
||||
const { urlTracker } = useDiscoverServices();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = savedSearchState.getCurrent$().subscribe((savedSearch) => {
|
||||
const dataView = savedSearch.searchSource.getField('index');
|
||||
|
||||
if (!dataView?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataViewSupportsTracking =
|
||||
// Disable for ad hoc data views, since they can't be restored after a page refresh
|
||||
dataView.isPersisted() ||
|
||||
// Unless it's a default profile data view, which can be restored on refresh
|
||||
internalState.getState().defaultProfileAdHocDataViewIds.includes(dataView.id) ||
|
||||
// Or we're in ES|QL mode, in which case we don't care about the data view
|
||||
isOfAggregateQueryType(savedSearch.searchSource.getField('query'));
|
||||
|
||||
const trackingEnabled = dataViewSupportsTracking || Boolean(savedSearch.id);
|
||||
|
||||
urlTracker.setTrackingEnabled(trackingEnabled);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [internalState, savedSearchState, urlTracker]);
|
||||
}
|
|
@ -350,8 +350,6 @@ export function getDataStateContainer({
|
|||
.subscribe();
|
||||
|
||||
return () => {
|
||||
abortController?.abort();
|
||||
abortControllerFetchMore?.abort();
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,15 +10,21 @@
|
|||
import { getSavedSearchContainer, isEqualSavedSearch } from './discover_saved_search_container';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search';
|
||||
import {
|
||||
createSavedSearchAdHocMock,
|
||||
createSavedSearchMock,
|
||||
savedSearchMock,
|
||||
savedSearchMockWithTimeField,
|
||||
} from '../../../__mocks__/saved_search';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { dataViewComplexMock } from '../../../__mocks__/data_view_complex';
|
||||
import { getDiscoverGlobalStateContainer } from './discover_global_state_container';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { VIEW_MODE } from '../../../../common/constants';
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { createInternalStateStore, createRuntimeStateManager } from './redux';
|
||||
import { createInternalStateStore, createRuntimeStateManager, internalStateActions } from './redux';
|
||||
import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
describe('DiscoverSavedSearchContainer', () => {
|
||||
const savedSearch = savedSearchMock;
|
||||
|
@ -37,7 +43,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const container = getSavedSearchContainer({
|
||||
services,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
expect(container.getTitle()).toBe(undefined);
|
||||
|
@ -47,7 +52,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const container = getSavedSearchContainer({
|
||||
services,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
container.set(savedSearch);
|
||||
|
@ -60,7 +64,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const container = getSavedSearchContainer({
|
||||
services,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
|
||||
|
@ -78,7 +81,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const container = getSavedSearchContainer({
|
||||
services,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
|
||||
|
@ -95,7 +97,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const savedSearchToPersist = {
|
||||
|
@ -122,7 +123,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
|
||||
|
@ -136,7 +136,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const savedSearchToPersist = {
|
||||
|
@ -157,7 +156,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const savedSearchToPersist = {
|
||||
|
@ -183,7 +181,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
savedSearchContainer.set(savedSearch);
|
||||
|
@ -206,7 +203,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
savedSearchContainer.set(savedSearch);
|
||||
|
@ -223,7 +219,6 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
|
||||
internalState,
|
||||
});
|
||||
const updated = savedSearchContainer.update({ nextDataView: dataViewMock });
|
||||
|
@ -289,4 +284,87 @@ describe('DiscoverSavedSearchContainer', () => {
|
|||
expect(isEqualSavedSearch(savedSearch1, savedSearch2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL tracking', () => {
|
||||
it('should enable URL tracking for a persisted data view', () => {
|
||||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
internalState,
|
||||
});
|
||||
const unsubscribe = savedSearchContainer.initUrlTracking();
|
||||
jest.spyOn(services.urlTracker, 'setTrackingEnabled').mockClear();
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
const currentSavedSearch = omit(createSavedSearchMock(), 'id');
|
||||
savedSearchContainer.set(currentSavedSearch);
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should disable URL tracking for an ad hoc data view', () => {
|
||||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
internalState,
|
||||
});
|
||||
const unsubscribe = savedSearchContainer.initUrlTracking();
|
||||
jest.spyOn(services.urlTracker, 'setTrackingEnabled').mockClear();
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
const currentSavedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
savedSearchContainer.set(currentSavedSearch);
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(false);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should enable URL tracking if the ad hoc data view is a default profile data view', () => {
|
||||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
internalState,
|
||||
});
|
||||
const unsubscribe = savedSearchContainer.initUrlTracking();
|
||||
jest.spyOn(services.urlTracker, 'setTrackingEnabled').mockClear();
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
const currentSavedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
internalState.dispatch(
|
||||
internalStateActions.setDefaultProfileAdHocDataViews([
|
||||
currentSavedSearch.searchSource.getField('index')!,
|
||||
])
|
||||
);
|
||||
savedSearchContainer.set(currentSavedSearch);
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should enable URL tracking with an ad hoc data view if in ES|QL mode', () => {
|
||||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
internalState,
|
||||
});
|
||||
const unsubscribe = savedSearchContainer.initUrlTracking();
|
||||
jest.spyOn(services.urlTracker, 'setTrackingEnabled').mockClear();
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
const currentSavedSearch = omit(createSavedSearchAdHocMock(), 'id');
|
||||
currentSavedSearch.searchSource.setField('query', { esql: 'FROM test' });
|
||||
savedSearchContainer.set(currentSavedSearch);
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('should enable URL tracking with an ad hoc data view if the saved search has an ID (persisted)', () => {
|
||||
const savedSearchContainer = getSavedSearchContainer({
|
||||
services: discoverServiceMock,
|
||||
globalStateContainer,
|
||||
internalState,
|
||||
});
|
||||
const unsubscribe = savedSearchContainer.initUrlTracking();
|
||||
jest.spyOn(services.urlTracker, 'setTrackingEnabled').mockClear();
|
||||
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
|
||||
const currentSavedSearch = createSavedSearchAdHocMock();
|
||||
savedSearchContainer.set(currentSavedSearch);
|
||||
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import type { FilterCompareOptions } from '@kbn/es-query';
|
||||
import { COMPARE_ALL_OPTIONS, updateFilterReferences } from '@kbn/es-query';
|
||||
import { COMPARE_ALL_OPTIONS, isOfAggregateQueryType, updateFilterReferences } from '@kbn/es-query';
|
||||
import type { SearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
|
||||
|
@ -58,6 +58,10 @@ export interface UpdateParams {
|
|||
* - useSavedSearchInitial for the persisted or initial state, just updated when the saved search is peristed or loaded
|
||||
*/
|
||||
export interface DiscoverSavedSearchContainer {
|
||||
/**
|
||||
* Enable/disable kbn url tracking (That's the URL used when selecting Discover in the side menu)
|
||||
*/
|
||||
initUrlTracking: () => () => void;
|
||||
/**
|
||||
* Get an BehaviorSubject which contains the current state of the current saved search
|
||||
* All modifications are applied to this state
|
||||
|
@ -148,6 +152,32 @@ export function getSavedSearchContainer({
|
|||
const getTitle = () => savedSearchCurrent$.getValue().title;
|
||||
const getId = () => savedSearchCurrent$.getValue().id;
|
||||
|
||||
const initUrlTracking = () => {
|
||||
const subscription = savedSearchCurrent$.subscribe((savedSearch) => {
|
||||
const dataView = savedSearch.searchSource.getField('index');
|
||||
|
||||
if (!dataView?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataViewSupportsTracking =
|
||||
// Disable for ad hoc data views, since they can't be restored after a page refresh
|
||||
dataView.isPersisted() ||
|
||||
// Unless it's a default profile data view, which can be restored on refresh
|
||||
internalState.getState().defaultProfileAdHocDataViewIds.includes(dataView.id) ||
|
||||
// Or we're in ES|QL mode, in which case we don't care about the data view
|
||||
isOfAggregateQueryType(savedSearch.searchSource.getField('query'));
|
||||
|
||||
const trackingEnabled = dataViewSupportsTracking || Boolean(savedSearch.id);
|
||||
|
||||
services.urlTracker.setTrackingEnabled(trackingEnabled);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
const persist = async (nextSavedSearch: SavedSearch, saveOptions?: SavedObjectSaveOpts) => {
|
||||
addLog('[savedSearch] persist', { nextSavedSearch, saveOptions });
|
||||
|
||||
|
@ -264,6 +294,7 @@ export function getSavedSearchContainer({
|
|||
};
|
||||
|
||||
return {
|
||||
initUrlTracking,
|
||||
getCurrent$,
|
||||
getHasChanged$,
|
||||
getId,
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
savedSearchMockWithTimeFieldNew,
|
||||
savedSearchMockWithESQL,
|
||||
} from '../../../__mocks__/saved_search';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { createDiscoverServicesMock } from '../../../__mocks__/services';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { getInitialState, type DiscoverAppStateContainer } from './discover_app_state_container';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
@ -36,6 +36,7 @@ import { createRuntimeStateManager } from './redux';
|
|||
import type { HistoryLocationState } from '../../../build_services';
|
||||
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
|
||||
import { updateSavedSearch } from './utils/update_saved_search';
|
||||
import { getConnectedCustomizationService } from '../../../customizations';
|
||||
|
||||
const startSync = (appState: DiscoverAppStateContainer) => {
|
||||
const { start, stop } = appState.syncState();
|
||||
|
@ -43,14 +44,15 @@ const startSync = (appState: DiscoverAppStateContainer) => {
|
|||
return stop;
|
||||
};
|
||||
|
||||
let mockServices = createDiscoverServicesMock();
|
||||
|
||||
async function getState(
|
||||
url: string = '/',
|
||||
{ savedSearch, isEmptyUrl }: { savedSearch?: SavedSearch; isEmptyUrl?: boolean } = {}
|
||||
) {
|
||||
const nextHistory = createBrowserHistory<HistoryLocationState>();
|
||||
nextHistory.push(url);
|
||||
|
||||
discoverServiceMock.dataViews.create = jest.fn().mockImplementation((spec) => {
|
||||
mockServices.dataViews.create = jest.fn().mockImplementation((spec) => {
|
||||
spec.id = spec.id ?? 'ad-hoc-id';
|
||||
spec.title = spec.title ?? 'test';
|
||||
return Promise.resolve({
|
||||
|
@ -65,6 +67,7 @@ async function getState(
|
|||
savedSearch: false,
|
||||
runtimeStateManager,
|
||||
history: nextHistory,
|
||||
services: mockServices,
|
||||
});
|
||||
jest.spyOn(nextState.dataState, 'fetch');
|
||||
await nextState.internalState.dispatch(internalStateActions.loadDataViewList());
|
||||
|
@ -72,12 +75,12 @@ async function getState(
|
|||
internalStateActions.setInitializationState({ hasESData: true, hasUserDataView: true })
|
||||
);
|
||||
if (savedSearch) {
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementation(() => {
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementation(() => {
|
||||
nextState.savedSearchState.set(copySavedSearch(savedSearch));
|
||||
return Promise.resolve(savedSearch);
|
||||
});
|
||||
} else {
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementation(() => {
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementation(() => {
|
||||
nextState.savedSearchState.set(copySavedSearch(savedSearchMockWithTimeFieldNew));
|
||||
return Promise.resolve(savedSearchMockWithTimeFieldNew);
|
||||
});
|
||||
|
@ -87,6 +90,10 @@ async function getState(
|
|||
return {
|
||||
history: nextHistory,
|
||||
state: nextState,
|
||||
customizationService: await getConnectedCustomizationService({
|
||||
customizationCallbacks: [],
|
||||
stateContainer: nextState,
|
||||
}),
|
||||
runtimeStateManager,
|
||||
getCurrentUrl,
|
||||
};
|
||||
|
@ -94,7 +101,7 @@ async function getState(
|
|||
|
||||
describe('Discover state', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockReset();
|
||||
mockServices = createDiscoverServicesMock();
|
||||
});
|
||||
|
||||
describe('Test discover state', () => {
|
||||
|
@ -240,8 +247,6 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
describe('Test discover initial state sort handling', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
test('Non-empty sort in URL should not be overwritten by saved search sort', async () => {
|
||||
const savedSearch = {
|
||||
...savedSearchMockWithTimeField,
|
||||
|
@ -259,26 +264,24 @@ describe('Discover state', () => {
|
|||
...savedSearchMock,
|
||||
...{ sort: [['bytes', 'desc']] as SortOrder[] },
|
||||
};
|
||||
const { state } = await getState('/', { savedSearch: nextSavedSearch });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: nextSavedSearch });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
expect(state.appState.getState().sort).toEqual([['bytes', 'desc']]);
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test discover state with legacy migration', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
test('migration of legacy query ', async () => {
|
||||
const { state } = await getState(
|
||||
"/#?_a=(query:(query_string:(analyze_wildcard:!t,query:'type:nice%20name:%22yeah%22')))",
|
||||
|
@ -314,8 +317,6 @@ describe('Discover state', () => {
|
|||
getSavedSearch: () => mockSavedSearch,
|
||||
});
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
describe('session name', () => {
|
||||
test('No persisted saved search returns default name', async () => {
|
||||
expect(await searchSessionInfoProvider.getName()).toBe('Discover');
|
||||
|
@ -387,12 +388,10 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
describe('Test discover searchSessionManager', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
test('getting the next session id', async () => {
|
||||
const { state } = await getState();
|
||||
const nextId = 'id';
|
||||
discoverServiceMock.data.search.session.start = jest.fn(() => nextId);
|
||||
mockServices.data.search.session.start = jest.fn(() => nextId);
|
||||
state.actions.initializeAndSync();
|
||||
expect(state.searchSessionManager.getNextSearchSessionId()).toBe(nextId);
|
||||
});
|
||||
|
@ -400,16 +399,16 @@ describe('Discover state', () => {
|
|||
|
||||
describe('Test discover state actions', () => {
|
||||
beforeEach(async () => {
|
||||
discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
|
||||
mockServices.data.query.timefilter.timefilter.getTime = jest.fn(() => {
|
||||
return { from: 'now-15d', to: 'now' };
|
||||
});
|
||||
discoverServiceMock.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
mockServices.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
return { pause: true, value: 1000 };
|
||||
});
|
||||
discoverServiceMock.data.search.searchSource.create = jest
|
||||
mockServices.data.search.searchSource.create = jest
|
||||
.fn()
|
||||
.mockReturnValue(savedSearchMock.searchSource);
|
||||
discoverServiceMock.core.savedObjects.client.resolve = jest.fn().mockReturnValue({
|
||||
mockServices.core.savedObjects.client.resolve = jest.fn().mockReturnValue({
|
||||
saved_object: {
|
||||
attributes: {
|
||||
kibanaSavedObjectMeta: {
|
||||
|
@ -460,7 +459,7 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('fetchData', async () => {
|
||||
const { state } = await getState('/');
|
||||
const { state, customizationService } = await getState('/');
|
||||
const dataState = state.dataState;
|
||||
await state.internalState.dispatch(internalStateActions.loadDataViewList());
|
||||
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.LOADING);
|
||||
|
@ -468,14 +467,13 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
state.actions.fetchData();
|
||||
await waitFor(() => {
|
||||
expect(dataState.data$.documents$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
});
|
||||
|
@ -491,12 +489,13 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch with no id given an empty URL', async () => {
|
||||
const { state, getCurrentUrl } = await getState('');
|
||||
const { state, customizationService, getCurrentUrl } = await getState('');
|
||||
await state.internalState.dispatch(internalStateActions.loadDataViewList());
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -505,7 +504,6 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
`"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())"`
|
||||
|
@ -534,11 +532,12 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadNewSavedSearch given an empty URL using loadSavedSearch', async () => {
|
||||
const { state, getCurrentUrl } = await getState('/');
|
||||
const { state, customizationService, getCurrentUrl } = await getState('/');
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -547,7 +546,6 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
`"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())"`
|
||||
|
@ -557,7 +555,7 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadNewSavedSearch with URL changing interval state', async () => {
|
||||
const { state, getCurrentUrl } = await getState(
|
||||
const { state, customizationService, getCurrentUrl } = await getState(
|
||||
'/#?_a=(interval:month,columns:!(bytes))&_g=()',
|
||||
{ isEmptyUrl: false }
|
||||
);
|
||||
|
@ -565,6 +563,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -573,7 +572,6 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
`"/#?_a=(columns:!(bytes),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"`
|
||||
|
@ -583,7 +581,7 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch with no id, given URL changes state', async () => {
|
||||
const { state, getCurrentUrl } = await getState(
|
||||
const { state, customizationService, getCurrentUrl } = await getState(
|
||||
'/#?_a=(interval:month,columns:!(bytes))&_g=()',
|
||||
{ isEmptyUrl: false }
|
||||
);
|
||||
|
@ -591,6 +589,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -599,7 +598,6 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
`"/#?_a=(columns:!(bytes),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"`
|
||||
|
@ -609,18 +607,20 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch given an empty URL, no state changes', async () => {
|
||||
const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock });
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const { state, customizationService, getCurrentUrl } = await getState('/', {
|
||||
savedSearch: savedSearchMock,
|
||||
});
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const savedSearch = copySavedSearch(savedSearchMock);
|
||||
const savedSearchWithDefaults = updateSavedSearch({
|
||||
savedSearch,
|
||||
state: getInitialState({
|
||||
initialUrlState: undefined,
|
||||
savedSearch,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
}),
|
||||
globalStateContainer: state.globalState,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
});
|
||||
return Promise.resolve(savedSearchWithDefaults);
|
||||
});
|
||||
|
@ -628,6 +628,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: 'the-saved-search-id',
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -635,7 +636,6 @@ describe('Discover state', () => {
|
|||
})
|
||||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(newSavedSearch?.id).toBe('the-saved-search-id');
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
|
@ -647,7 +647,7 @@ describe('Discover state', () => {
|
|||
|
||||
test('loadSavedSearch given a URL with different interval and columns modifying the state', async () => {
|
||||
const url = '/#?_a=(interval:month,columns:!(message))&_g=()';
|
||||
const { state, getCurrentUrl } = await getState(url, {
|
||||
const { state, customizationService, getCurrentUrl } = await getState(url, {
|
||||
savedSearch: savedSearchMock,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
|
@ -655,13 +655,13 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||
`"/#?_a=(columns:!(message),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"`
|
||||
|
@ -678,7 +678,7 @@ describe('Discover state', () => {
|
|||
timeRestore: true,
|
||||
timeRange: { from: 'now-15d', to: 'now' },
|
||||
};
|
||||
const { state } = await getState(url, {
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
|
@ -686,13 +686,13 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true);
|
||||
state.actions.stopSyncing();
|
||||
|
@ -700,7 +700,7 @@ describe('Discover state', () => {
|
|||
|
||||
test('loadSavedSearch given a URL with different refresh interval than the stored one showing as changed', async () => {
|
||||
const url = '/#_g=(time:(from:now-15d,to:now),refreshInterval:(pause:!f,value:1234))';
|
||||
discoverServiceMock.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
mockServices.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
return { pause: false, value: 1234 };
|
||||
});
|
||||
const savedSearch = {
|
||||
|
@ -710,7 +710,7 @@ describe('Discover state', () => {
|
|||
timeRange: { from: 'now-15d', to: 'now' },
|
||||
refreshInterval: { pause: false, value: 60000 },
|
||||
};
|
||||
const { state } = await getState(url, {
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
|
@ -718,13 +718,13 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true);
|
||||
state.actions.stopSyncing();
|
||||
|
@ -732,7 +732,7 @@ describe('Discover state', () => {
|
|||
|
||||
test('loadSavedSearch given a URL with matching time range and refresh interval not showing as changed', async () => {
|
||||
const url = '/#?_g=(time:(from:now-15d,to:now),refreshInterval:(pause:!f,value:60000))';
|
||||
discoverServiceMock.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
mockServices.data.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
|
||||
return { pause: false, value: 60000 };
|
||||
});
|
||||
const savedSearch = {
|
||||
|
@ -742,7 +742,7 @@ describe('Discover state', () => {
|
|||
timeRange: { from: 'now-15d', to: 'now' },
|
||||
refreshInterval: { pause: false, value: 60000 },
|
||||
};
|
||||
const { state } = await getState(url, {
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
|
@ -750,13 +750,13 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
state.actions.stopSyncing();
|
||||
|
@ -764,11 +764,12 @@ describe('Discover state', () => {
|
|||
|
||||
test('loadSavedSearch ignoring hideChart in URL', async () => {
|
||||
const url = '/#?_a=(hideChart:true,columns:!(message))&_g=()';
|
||||
const { state } = await getState(url, { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState(url, { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -781,11 +782,15 @@ describe('Discover state', () => {
|
|||
|
||||
test('loadSavedSearch without id ignoring invalid index in URL, adding a warning toast', async () => {
|
||||
const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()';
|
||||
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch: savedSearchMock,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -795,7 +800,7 @@ describe('Discover state', () => {
|
|||
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
|
||||
'the-data-view-id'
|
||||
);
|
||||
expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith(
|
||||
expect(mockServices.toastNotifications.addWarning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
'data-test-subj': 'dscDataViewNotFoundShowDefaultWarning',
|
||||
})
|
||||
|
@ -805,11 +810,15 @@ describe('Discover state', () => {
|
|||
test('loadSavedSearch without id containing ES|QL, adding no warning toast with an invalid index', async () => {
|
||||
const url =
|
||||
"/#?_a=(dataSource:(dataViewId:abcde,type:dataView),query:(esql:'FROM test'))&_g=()";
|
||||
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch: savedSearchMock,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -817,16 +826,20 @@ describe('Discover state', () => {
|
|||
})
|
||||
);
|
||||
expect(state.appState.getState().dataSource).toEqual(createEsqlDataSource());
|
||||
expect(discoverServiceMock.toastNotifications.addWarning).not.toHaveBeenCalled();
|
||||
expect(mockServices.toastNotifications.addWarning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('loadSavedSearch with id ignoring invalid index in URL, adding a warning toast', async () => {
|
||||
const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()';
|
||||
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch: savedSearchMock,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -836,7 +849,7 @@ describe('Discover state', () => {
|
|||
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
|
||||
'the-data-view-id'
|
||||
);
|
||||
expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith(
|
||||
expect(mockServices.toastNotifications.addWarning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
'data-test-subj': 'dscDataViewNotFoundShowSavedWarning',
|
||||
})
|
||||
|
@ -844,18 +857,20 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch data view handling', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const { state, customizationService, history } = await getState('/', {
|
||||
savedSearch: savedSearchMock,
|
||||
});
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const savedSearch = copySavedSearch(savedSearchMock);
|
||||
const savedSearchWithDefaults = updateSavedSearch({
|
||||
savedSearch,
|
||||
state: getInitialState({
|
||||
initialUrlState: undefined,
|
||||
savedSearch,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
}),
|
||||
globalStateContainer: state.globalState,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
});
|
||||
return Promise.resolve(savedSearchWithDefaults);
|
||||
});
|
||||
|
@ -863,6 +878,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -873,27 +889,29 @@ describe('Discover state', () => {
|
|||
'the-data-view-id'
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const savedSearch = copySavedSearch(savedSearchMockWithTimeField);
|
||||
const savedSearchWithDefaults = updateSavedSearch({
|
||||
savedSearch,
|
||||
state: getInitialState({
|
||||
initialUrlState: undefined,
|
||||
savedSearch,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
}),
|
||||
globalStateContainer: state.globalState,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
});
|
||||
return Promise.resolve(savedSearchWithDefaults);
|
||||
});
|
||||
history.push('/');
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: 'the-saved-search-id-with-timefield',
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
defaultUrlState: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -901,17 +919,17 @@ describe('Discover state', () => {
|
|||
'index-pattern-with-timefield-id'
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
jest.spyOn(discoverServiceMock.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
jest.spyOn(mockServices.savedSearch, 'get').mockImplementationOnce(() => {
|
||||
const savedSearch = copySavedSearch(savedSearchMock);
|
||||
const savedSearchWithDefaults = updateSavedSearch({
|
||||
savedSearch,
|
||||
state: getInitialState({
|
||||
initialUrlState: undefined,
|
||||
savedSearch,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
}),
|
||||
globalStateContainer: state.globalState,
|
||||
services: discoverServiceMock,
|
||||
services: mockServices,
|
||||
});
|
||||
return Promise.resolve(savedSearchWithDefaults);
|
||||
});
|
||||
|
@ -919,6 +937,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: {
|
||||
|
@ -936,13 +955,13 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch generating a new saved search, updated by ad-hoc data view', async () => {
|
||||
const { state } = await getState('/');
|
||||
const { state, customizationService } = await getState('/');
|
||||
const dataViewSpecMock = {
|
||||
id: 'mock-id',
|
||||
title: 'mock-title',
|
||||
timeFieldName: 'mock-time-field-name',
|
||||
};
|
||||
const dataViewsCreateMock = discoverServiceMock.dataViews.create as jest.Mock;
|
||||
const dataViewsCreateMock = mockServices.dataViews.create as jest.Mock;
|
||||
dataViewsCreateMock.mockImplementationOnce(() => ({
|
||||
...dataViewMock,
|
||||
...dataViewSpecMock,
|
||||
|
@ -952,6 +971,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: dataViewSpecMock,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -971,19 +991,20 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('loadSavedSearch resetting query & filters of data service', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(discoverServiceMock.data.query.queryString.clearQuery).toHaveBeenCalled();
|
||||
expect(discoverServiceMock.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]);
|
||||
expect(mockServices.data.query.queryString.clearQuery).toHaveBeenCalled();
|
||||
expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
test('loadSavedSearch setting query & filters of data service if query and filters are persisted', async () => {
|
||||
|
@ -992,31 +1013,35 @@ describe('Discover state', () => {
|
|||
const filters = [{ meta: { index: 'the-data-view-id' }, query: { match_all: {} } }];
|
||||
savedSearchWithQueryAndFilters.searchSource.setField('query', query);
|
||||
savedSearchWithQueryAndFilters.searchSource.setField('filter', filters);
|
||||
const { state } = await getState('/', { savedSearch: savedSearchWithQueryAndFilters });
|
||||
const { state, customizationService } = await getState('/', {
|
||||
savedSearch: savedSearchWithQueryAndFilters,
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(discoverServiceMock.data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
||||
expect(discoverServiceMock.data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
|
||||
filters
|
||||
);
|
||||
expect(mockServices.data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
||||
expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith(filters);
|
||||
});
|
||||
|
||||
test('loadSavedSearch with ad-hoc data view being added to internal state adHocDataViews', async () => {
|
||||
const savedSearchAdHocCopy = copySavedSearch(savedSearchAdHoc);
|
||||
const adHocDataViewId = savedSearchAdHoc.searchSource.getField('index')!.id;
|
||||
const { state } = await getState('/', { savedSearch: savedSearchAdHocCopy });
|
||||
const { state, customizationService } = await getState('/', {
|
||||
savedSearch: savedSearchAdHocCopy,
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchAdHoc.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1033,7 +1058,7 @@ describe('Discover state', () => {
|
|||
const savedSearchMockWithESQLCopy = copySavedSearch(savedSearchMockWithESQL);
|
||||
const persistedDataViewId = savedSearchMockWithESQLCopy?.searchSource.getField('index')!.id;
|
||||
const url = "/#?_a=(dataSource:(dataViewId:'the-data-view-id',type:dataView))&_g=()";
|
||||
const { state } = await getState(url, {
|
||||
const { state, customizationService } = await getState(url, {
|
||||
savedSearch: savedSearchMockWithESQLCopy,
|
||||
isEmptyUrl: false,
|
||||
});
|
||||
|
@ -1041,6 +1066,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMockWithESQL.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1080,23 +1106,25 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onChangeDataView', async () => {
|
||||
const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService, getCurrentUrl } = await getState('/', {
|
||||
savedSearch: savedSearchMock,
|
||||
});
|
||||
const { actions, savedSearchState, dataState } = state;
|
||||
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
// test initial state
|
||||
expect(dataState.fetch).toHaveBeenCalledTimes(0);
|
||||
expect(dataState.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(savedSearchState.getState().searchSource.getField('index')!.id).toBe(dataViewMock.id);
|
||||
expect(getCurrentUrl()).toContain(dataViewMock.id);
|
||||
|
||||
|
@ -1105,7 +1133,7 @@ describe('Discover state', () => {
|
|||
await new Promise(process.nextTick);
|
||||
|
||||
// test changed state, fetch should be called once and URL should be updated
|
||||
expect(dataState.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(dataState.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(state.appState.getState().dataSource).toEqual(
|
||||
createDataViewDataSource({ dataViewId: dataViewComplexMock.id! })
|
||||
);
|
||||
|
@ -1118,18 +1146,18 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onDataViewCreated - persisted data view', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.onDataViewCreated(dataViewComplexMock);
|
||||
await waitFor(() => {
|
||||
expect(state.getCurrentTab().dataViewId).toBe(dataViewComplexMock.id);
|
||||
|
@ -1144,20 +1172,20 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onDataViewCreated - ad-hoc data view', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
jest
|
||||
.spyOn(discoverServiceMock.dataViews, 'get')
|
||||
.spyOn(mockServices.dataViews, 'get')
|
||||
.mockImplementationOnce((id) =>
|
||||
id === dataViewAdHoc.id ? Promise.resolve(dataViewAdHoc) : Promise.reject()
|
||||
);
|
||||
|
@ -1175,11 +1203,12 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onDataViewEdited - persisted data view', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1188,7 +1217,6 @@ describe('Discover state', () => {
|
|||
);
|
||||
const selectedDataViewId = state.getCurrentTab().dataViewId;
|
||||
expect(selectedDataViewId).toBe(dataViewMock.id);
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.onDataViewEdited(dataViewMock);
|
||||
await waitFor(() => {
|
||||
expect(state.getCurrentTab().dataViewId).toBe(selectedDataViewId);
|
||||
|
@ -1209,12 +1237,12 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onOpenSavedSearch - same target id', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
state.actions.initializeAndSync();
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1229,14 +1257,18 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onOpenSavedSearch - cleanup of previous filter', async () => {
|
||||
const { state, history } = await getState(
|
||||
const { state, customizationService, history } = await getState(
|
||||
"/#?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))&_a=(columns:!(customer_first_name),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,key:customer_first_name,negate:!f,params:(query:Mary),type:phrase),query:(match_phrase:(customer_first_name:Mary)))),hideChart:!f,index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,interval:auto,query:(language:kuery,query:''),sort:!())",
|
||||
{ savedSearch: savedSearchMock, isEmptyUrl: false }
|
||||
);
|
||||
jest.spyOn(mockServices.filterManager, 'getAppFilters').mockImplementation(() => {
|
||||
return state.appState.getState().filters!;
|
||||
});
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1249,6 +1281,7 @@ describe('Discover state', () => {
|
|||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: undefined,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
@ -1259,18 +1292,18 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('onCreateDefaultAdHocDataView', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.createAndAppendAdHocDataView({ title: 'ad-hoc-test' });
|
||||
expect(state.appState.getState().dataSource).toEqual(
|
||||
createDataViewDataSource({ dataViewId: 'ad-hoc-id' })
|
||||
|
@ -1280,19 +1313,21 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('undoSavedSearchChanges - when changing data views', async () => {
|
||||
const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const { state, customizationService, getCurrentUrl } = await getState('/', {
|
||||
savedSearch: savedSearchMock,
|
||||
});
|
||||
// Load a given persisted saved search
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
const initialUrlState =
|
||||
'/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())';
|
||||
|
@ -1306,7 +1341,7 @@ describe('Discover state', () => {
|
|||
`"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(),dataSource:(dataViewId:data-view-with-various-field-types-id,type:dataView),interval:auto,sort:!(!(data,desc)))"`
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(state.getCurrentTab().dataViewId).toBe(dataViewComplexMock.id!);
|
||||
|
||||
|
@ -1315,7 +1350,7 @@ describe('Discover state', () => {
|
|||
await new Promise(process.nextTick);
|
||||
expect(getCurrentUrl()).toBe(initialUrlState);
|
||||
await waitFor(() => {
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
expect(state.getCurrentTab().dataViewId).toBe(dataViewMock.id!);
|
||||
|
||||
|
@ -1323,7 +1358,7 @@ describe('Discover state', () => {
|
|||
});
|
||||
|
||||
test('undoSavedSearchChanges with timeRestore', async () => {
|
||||
const { state } = await getState('/', {
|
||||
const { state, customizationService } = await getState('/', {
|
||||
savedSearch: {
|
||||
...savedSearchMockWithTimeField,
|
||||
timeRestore: true,
|
||||
|
@ -1333,12 +1368,13 @@ describe('Discover state', () => {
|
|||
});
|
||||
const setTime = jest.fn();
|
||||
const setRefreshInterval = jest.fn();
|
||||
discoverServiceMock.data.query.timefilter.timefilter.setTime = setTime;
|
||||
discoverServiceMock.data.query.timefilter.timefilter.setRefreshInterval = setRefreshInterval;
|
||||
mockServices.data.query.timefilter.timefilter.setTime = setTime;
|
||||
mockServices.data.query.timefilter.timefilter.setRefreshInterval = setRefreshInterval;
|
||||
await state.internalState.dispatch(
|
||||
state.injectCurrentTab(internalStateActions.initializeSession)({
|
||||
initializeSessionParams: {
|
||||
stateContainer: state,
|
||||
customizationService,
|
||||
discoverSessionId: savedSearchMock.id,
|
||||
dataViewSpec: undefined,
|
||||
defaultUrlState: undefined,
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { IKbnUrlStateStorage, StateContainer } from '@kbn/kibana-utils-plugin/public';
|
||||
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { DataPublicPluginStart, SearchSessionInfoProvider } from '@kbn/data-plugin/public';
|
||||
import { noSearchSessionStorageCapabilityMessage } from '@kbn/data-plugin/public';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
|
@ -71,9 +70,9 @@ export interface DiscoverStateContainerParams {
|
|||
*/
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
/**
|
||||
* a custom url state storage
|
||||
* URL state storage
|
||||
*/
|
||||
stateStorageContainer?: IKbnUrlStateStorage;
|
||||
stateStorageContainer: IKbnUrlStateStorage;
|
||||
/**
|
||||
* Internal shared state that's used at several places in the UI
|
||||
*/
|
||||
|
@ -240,27 +239,13 @@ export function getDiscoverStateContainer({
|
|||
tabId,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer,
|
||||
stateStorageContainer: stateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
}: DiscoverStateContainerParams): DiscoverStateContainer {
|
||||
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
|
||||
const toasts = services.core.notifications.toasts;
|
||||
const injectCurrentTab = createTabActionInjector(tabId);
|
||||
const getCurrentTab = () => selectTab(internalState.getState(), tabId);
|
||||
|
||||
/**
|
||||
* state storage for state in the URL
|
||||
*/
|
||||
const stateStorage =
|
||||
stateStorageContainer ??
|
||||
createKbnUrlStateStorage({
|
||||
useHash: storeInSessionStorage,
|
||||
history: services.history,
|
||||
useHashQuery: customizationContext.displayMode !== 'embedded',
|
||||
...(toasts && withNotifyOnErrors(toasts)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Search session logic
|
||||
*/
|
||||
|
@ -456,6 +441,9 @@ export function getDiscoverStateContainer({
|
|||
savedSearchContainer.updateTimeRange();
|
||||
});
|
||||
|
||||
// Enable/disable kbn url tracking (That's the URL used when selecting Discover in the side menu)
|
||||
const unsubscribeSavedSearchUrlTracking = savedSearchContainer.initUrlTracking();
|
||||
|
||||
// initialize app state container, syncing with _g and _a part of the URL
|
||||
const appStateInitAndSyncUnsubscribe = appStateContainer.initAndSync();
|
||||
|
||||
|
@ -508,6 +496,7 @@ export function getDiscoverStateContainer({
|
|||
unsubscribeData();
|
||||
appStateUnsubscribe();
|
||||
appStateInitAndSyncUnsubscribe();
|
||||
unsubscribeSavedSearchUrlTracking();
|
||||
filterUnsubscribe.unsubscribe();
|
||||
timefilerUnsubscribe.unsubscribe();
|
||||
};
|
||||
|
|
|
@ -35,9 +35,12 @@ import { getValidFilters } from '../../../../../utils/get_valid_filters';
|
|||
import { updateSavedSearch } from '../../utils/update_saved_search';
|
||||
import { APP_STATE_URL_KEY } from '../../../../../../common';
|
||||
import { selectTabRuntimeState } from '../runtime_state';
|
||||
import type { ConnectedCustomizationService } from '../../../../../customizations';
|
||||
import { disconnectTab } from './tabs';
|
||||
|
||||
export interface InitializeSessionParams {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
customizationService: ConnectedCustomizationService;
|
||||
discoverSessionId: string | undefined;
|
||||
dataViewSpec: DataViewSpec | undefined;
|
||||
defaultUrlState: DiscoverAppState | undefined;
|
||||
|
@ -49,13 +52,20 @@ export const initializeSession: InternalStateThunkActionCreator<
|
|||
> =
|
||||
({
|
||||
tabId,
|
||||
initializeSessionParams: { stateContainer, discoverSessionId, dataViewSpec, defaultUrlState },
|
||||
initializeSessionParams: {
|
||||
stateContainer,
|
||||
customizationService,
|
||||
discoverSessionId,
|
||||
dataViewSpec,
|
||||
defaultUrlState,
|
||||
},
|
||||
}) =>
|
||||
async (
|
||||
dispatch,
|
||||
getState,
|
||||
{ services, customizationContext, runtimeStateManager, urlStateStorage }
|
||||
) => {
|
||||
dispatch(disconnectTab({ tabId }));
|
||||
dispatch(internalStateSlice.actions.resetOnSavedSearchChange({ tabId }));
|
||||
|
||||
/**
|
||||
|
@ -113,11 +123,13 @@ export const initializeSession: InternalStateThunkActionCreator<
|
|||
setBreadcrumbs({ services, titleBreadcrumbText: persistedDiscoverSession.title });
|
||||
}
|
||||
|
||||
const { currentDataView$, stateContainer$, customizationService$ } = selectTabRuntimeState(
|
||||
runtimeStateManager,
|
||||
tabId
|
||||
);
|
||||
let dataView: DataView;
|
||||
|
||||
if (isOfAggregateQueryType(initialQuery)) {
|
||||
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, tabId);
|
||||
|
||||
// Regardless of what was requested, we always use ad hoc data views for ES|QL
|
||||
dataView = await getEsqlDataView(
|
||||
initialQuery,
|
||||
|
@ -239,6 +251,12 @@ export const initializeSession: InternalStateThunkActionCreator<
|
|||
// Make sure app state container is completely reset
|
||||
stateContainer.appState.resetToState(initialState);
|
||||
stateContainer.appState.resetInitialState();
|
||||
stateContainer$.next(stateContainer);
|
||||
customizationService$.next(customizationService);
|
||||
|
||||
// Begin syncing the state and trigger the initial fetch
|
||||
stateContainer.actions.initializeAndSync();
|
||||
stateContainer.actions.fetchData(true);
|
||||
discoverSessionLoadTracker.reportEvent();
|
||||
|
||||
return { showNoDataPage: false };
|
||||
|
|
|
@ -8,15 +8,16 @@
|
|||
*/
|
||||
|
||||
import type { TabbedContentState } from '@kbn/unified-tabs/src/components/tabbed_content/tabbed_content';
|
||||
import { differenceBy } from 'lodash';
|
||||
import { cloneDeep, differenceBy } from 'lodash';
|
||||
import type { TabState } from '../types';
|
||||
import { selectAllTabs, selectTab } from '../selectors';
|
||||
import {
|
||||
defaultTabState,
|
||||
internalStateSlice,
|
||||
type TabActionPayload,
|
||||
type InternalStateThunkActionCreator,
|
||||
} from '../internal_state';
|
||||
import { createTabRuntimeState } from '../runtime_state';
|
||||
import { createTabRuntimeState, selectTabRuntimeState } from '../runtime_state';
|
||||
|
||||
export const setTabs: InternalStateThunkActionCreator<
|
||||
[Parameters<typeof internalStateSlice.actions.setTabs>[0]]
|
||||
|
@ -28,6 +29,7 @@ export const setTabs: InternalStateThunkActionCreator<
|
|||
const addedTabs = differenceBy(params.allTabs, previousTabs, (tab) => tab.id);
|
||||
|
||||
for (const tab of removedTabs) {
|
||||
dispatch(disconnectTab({ tabId: tab.id }));
|
||||
delete runtimeStateManager.tabs.byId[tab.id];
|
||||
}
|
||||
|
||||
|
@ -38,24 +40,20 @@ export const setTabs: InternalStateThunkActionCreator<
|
|||
dispatch(internalStateSlice.actions.setTabs(params));
|
||||
};
|
||||
|
||||
export interface UpdateTabsParams {
|
||||
currentTabId: string;
|
||||
updateState: TabbedContentState;
|
||||
stopSyncing?: () => void;
|
||||
}
|
||||
|
||||
export const updateTabs: InternalStateThunkActionCreator<[UpdateTabsParams], Promise<void>> =
|
||||
({ currentTabId, updateState: { items, selectedItem }, stopSyncing }) =>
|
||||
async (dispatch, getState, { urlStateStorage }) => {
|
||||
export const updateTabs: InternalStateThunkActionCreator<[TabbedContentState], Promise<void>> =
|
||||
({ items, selectedItem }) =>
|
||||
async (dispatch, getState, { services, runtimeStateManager, urlStateStorage }) => {
|
||||
const currentState = getState();
|
||||
const currentTab = selectTab(currentState, currentTabId);
|
||||
const currentTab = selectTab(currentState, currentState.tabs.unsafeCurrentId);
|
||||
let updatedTabs = items.map<TabState>((item) => {
|
||||
const existingTab = currentState.tabs.byId[item.id];
|
||||
const existingTab = selectTab(currentState, item.id);
|
||||
return existingTab ? { ...existingTab, ...item } : { ...defaultTabState, ...item };
|
||||
});
|
||||
|
||||
if (selectedItem?.id !== currentTab.id) {
|
||||
stopSyncing?.();
|
||||
const previousTabRuntimeState = selectTabRuntimeState(runtimeStateManager, currentTab.id);
|
||||
|
||||
previousTabRuntimeState.stateContainer$.getValue()?.actions.stopSyncing();
|
||||
|
||||
updatedTabs = updatedTabs.map((tab) =>
|
||||
tab.id === currentTab.id
|
||||
|
@ -67,16 +65,57 @@ export const updateTabs: InternalStateThunkActionCreator<[UpdateTabsParams], Pro
|
|||
: tab
|
||||
);
|
||||
|
||||
const existingTab = selectedItem ? currentState.tabs.byId[selectedItem.id] : undefined;
|
||||
const nextTab = selectedItem ? selectTab(currentState, selectedItem.id) : undefined;
|
||||
|
||||
if (existingTab) {
|
||||
await urlStateStorage.set('_g', existingTab.globalState);
|
||||
await urlStateStorage.set('_a', existingTab.appState);
|
||||
if (nextTab) {
|
||||
await urlStateStorage.set('_g', nextTab.globalState);
|
||||
await urlStateStorage.set('_a', nextTab.appState);
|
||||
} else {
|
||||
await urlStateStorage.set('_g', {});
|
||||
await urlStateStorage.set('_a', {});
|
||||
await urlStateStorage.set('_g', null);
|
||||
await urlStateStorage.set('_a', null);
|
||||
}
|
||||
|
||||
const nextTabRuntimeState = selectedItem
|
||||
? selectTabRuntimeState(runtimeStateManager, selectedItem.id)
|
||||
: undefined;
|
||||
const nextTabStateContainer = nextTabRuntimeState?.stateContainer$.getValue();
|
||||
|
||||
if (nextTabStateContainer) {
|
||||
const {
|
||||
time,
|
||||
refreshInterval,
|
||||
filters: globalFilters,
|
||||
} = nextTabStateContainer.globalState.get() ?? {};
|
||||
const { filters: appFilters, query } = nextTabStateContainer.appState.getState();
|
||||
|
||||
services.timefilter.setTime(time ?? services.timefilter.getTimeDefaults());
|
||||
services.timefilter.setRefreshInterval(
|
||||
refreshInterval ?? services.timefilter.getRefreshIntervalDefaults()
|
||||
);
|
||||
services.filterManager.setGlobalFilters(globalFilters ?? []);
|
||||
services.filterManager.setAppFilters(cloneDeep(appFilters ?? []));
|
||||
services.data.query.queryString.setQuery(
|
||||
query ?? services.data.query.queryString.getDefaultQuery()
|
||||
);
|
||||
|
||||
nextTabStateContainer.actions.initializeAndSync();
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setTabs({ allTabs: updatedTabs }));
|
||||
dispatch(
|
||||
setTabs({
|
||||
allTabs: updatedTabs,
|
||||
selectedTabId: selectedItem?.id ?? currentTab.id,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const disconnectTab: InternalStateThunkActionCreator<[TabActionPayload]> =
|
||||
({ tabId }) =>
|
||||
(_, __, { runtimeStateManager }) => {
|
||||
const tabRuntimeState = selectTabRuntimeState(runtimeStateManager, tabId);
|
||||
const stateContainer = tabRuntimeState.stateContainer$.getValue();
|
||||
stateContainer?.dataState.cancel();
|
||||
stateContainer?.actions.stopSyncing();
|
||||
tabRuntimeState.customizationService$.getValue()?.cleanup();
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
setDefaultProfileAdHocDataViews,
|
||||
setTabs,
|
||||
updateTabs,
|
||||
disconnectTab,
|
||||
} from './actions';
|
||||
|
||||
export type { DiscoverInternalState, TabState, InternalStateDataRequestParams } from './types';
|
||||
|
@ -35,6 +36,7 @@ export const internalStateActions = {
|
|||
loadDataViewList,
|
||||
setTabs,
|
||||
updateTabs,
|
||||
disconnectTab,
|
||||
setDataView,
|
||||
setAdHocDataViews,
|
||||
setDefaultProfileAdHocDataViews,
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
type TabState,
|
||||
} from './types';
|
||||
import { loadDataViewList, setTabs } from './actions';
|
||||
import { selectAllTabs } from './selectors';
|
||||
import { selectAllTabs, selectTab } from './selectors';
|
||||
import { createTabItem } from './utils';
|
||||
|
||||
export const defaultTabState: Omit<TabState, keyof TabItem> = {
|
||||
|
@ -62,7 +62,7 @@ const initialState: DiscoverInternalState = {
|
|||
savedDataViews: [],
|
||||
expandedDoc: undefined,
|
||||
isESQLToDataViewTransitionModalVisible: false,
|
||||
tabs: { byId: {}, allIds: [] },
|
||||
tabs: { byId: {}, allIds: [], unsafeCurrentId: '' },
|
||||
};
|
||||
|
||||
export type TabActionPayload<T extends { [key: string]: unknown } = {}> = { tabId: string } & T;
|
||||
|
@ -74,7 +74,7 @@ const withTab = <TAction extends TabAction>(
|
|||
action: TAction,
|
||||
fn: (tab: TabState) => void
|
||||
) => {
|
||||
const tab = state.tabs.byId[action.payload.tabId];
|
||||
const tab = selectTab(state, action.payload.tabId);
|
||||
|
||||
if (tab) {
|
||||
fn(tab);
|
||||
|
@ -92,7 +92,7 @@ export const internalStateSlice = createSlice({
|
|||
state.initializationState = action.payload;
|
||||
},
|
||||
|
||||
setTabs: (state, action: PayloadAction<{ allTabs: TabState[] }>) => {
|
||||
setTabs: (state, action: PayloadAction<{ allTabs: TabState[]; selectedTabId: string }>) => {
|
||||
state.tabs.byId = action.payload.allTabs.reduce<Record<string, TabState>>(
|
||||
(acc, tab) => ({
|
||||
...acc,
|
||||
|
@ -101,6 +101,7 @@ export const internalStateSlice = createSlice({
|
|||
{}
|
||||
);
|
||||
state.tabs.allIds = action.payload.allTabs.map((tab) => tab.id);
|
||||
state.tabs.unsafeCurrentId = action.payload.selectedTabId;
|
||||
},
|
||||
|
||||
setDataViewId: (state, action: TabAction<{ dataViewId: string | undefined }>) =>
|
||||
|
@ -203,7 +204,7 @@ export const createInternalStateStore = (options: InternalStateThunkDependencies
|
|||
...defaultTabState,
|
||||
...createTabItem(selectAllTabs(store.getState())),
|
||||
};
|
||||
store.dispatch(setTabs({ allTabs: [defaultTab] }));
|
||||
store.dispatch(setTabs({ allTabs: [defaultTab], selectedTabId: defaultTab.id }));
|
||||
|
||||
return store;
|
||||
};
|
||||
|
|
|
@ -12,12 +12,16 @@ import React, { type PropsWithChildren, createContext, useContext, useMemo } fro
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { useCurrentTabContext } from './hooks';
|
||||
import type { DiscoverStateContainer } from '../discover_state';
|
||||
import type { ConnectedCustomizationService } from '../../../../customizations';
|
||||
|
||||
interface DiscoverRuntimeState {
|
||||
adHocDataViews: DataView[];
|
||||
}
|
||||
|
||||
interface TabRuntimeState {
|
||||
stateContainer?: DiscoverStateContainer;
|
||||
customizationService?: ConnectedCustomizationService;
|
||||
currentDataView: DataView;
|
||||
}
|
||||
|
||||
|
@ -39,6 +43,8 @@ export const createRuntimeStateManager = (): RuntimeStateManager => ({
|
|||
});
|
||||
|
||||
export const createTabRuntimeState = (): ReactiveTabRuntimeState => ({
|
||||
stateContainer$: new BehaviorSubject<DiscoverStateContainer | undefined>(undefined),
|
||||
customizationService$: new BehaviorSubject<ConnectedCustomizationService | undefined>(undefined),
|
||||
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import type { DiscoverInternalState } from './types';
|
||||
|
||||
export const selectAllTabs = (state: DiscoverInternalState) =>
|
||||
state.tabs.allIds.map((id) => state.tabs.byId[id]);
|
||||
|
||||
export const selectTab = (state: DiscoverInternalState, tabId: string) => state.tabs.byId[tabId];
|
||||
|
||||
export const selectAllTabs = (state: DiscoverInternalState) =>
|
||||
state.tabs.allIds.map((id) => selectTab(state, id));
|
||||
|
|
|
@ -70,5 +70,13 @@ export interface DiscoverInternalState {
|
|||
tabs: {
|
||||
byId: Record<string, TabState>;
|
||||
allIds: string[];
|
||||
/**
|
||||
* WARNING: You probably don't want to use this property.
|
||||
* This is used high in the component tree for managing tabs,
|
||||
* but is unsafe to use in actions and selectors since it can
|
||||
* change between renders and leak state between tabs.
|
||||
* Actions and selectors should use a tab ID parameter instead.
|
||||
*/
|
||||
unsafeCurrentId: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,46 +11,42 @@ import { renderHook, act } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { getDiscoverStateMock } from '../__mocks__/discover_state.mock';
|
||||
import {
|
||||
type ConnectedCustomizationService,
|
||||
DiscoverCustomizationProvider,
|
||||
getConnectedCustomizationService,
|
||||
useDiscoverCustomization,
|
||||
useDiscoverCustomization$,
|
||||
useDiscoverCustomizationService,
|
||||
} from './customization_provider';
|
||||
import type {
|
||||
DiscoverCustomization,
|
||||
DiscoverCustomizationId,
|
||||
DiscoverCustomizationService,
|
||||
} from './customization_service';
|
||||
import type { DiscoverCustomization, DiscoverCustomizationId } from './customization_service';
|
||||
import { createCustomizationService } from './customization_service';
|
||||
import type { CustomizationCallback } from './types';
|
||||
|
||||
describe('useDiscoverCustomizationService', () => {
|
||||
describe('getConnectedCustomizationService', () => {
|
||||
it('should provide customization service', async () => {
|
||||
let resolveCallback = (_: () => void) => {};
|
||||
const promise = new Promise<() => void>((resolve) => {
|
||||
resolveCallback = resolve;
|
||||
});
|
||||
let service: DiscoverCustomizationService | undefined;
|
||||
const callback = jest.fn(({ customizations }) => {
|
||||
service = customizations;
|
||||
return promise;
|
||||
});
|
||||
const customizationCallbacks: CustomizationCallback[] = [callback];
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
const wrapper = renderHook(() =>
|
||||
useDiscoverCustomizationService({ stateContainer, customizationCallbacks })
|
||||
);
|
||||
expect(wrapper.result.current).toBeUndefined();
|
||||
const servicePromise = getConnectedCustomizationService({
|
||||
stateContainer,
|
||||
customizationCallbacks,
|
||||
});
|
||||
let service: ConnectedCustomizationService | undefined;
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
const cleanup = jest.fn();
|
||||
await act(async () => {
|
||||
resolveCallback(cleanup);
|
||||
await promise;
|
||||
service = await servicePromise;
|
||||
});
|
||||
expect(wrapper.result.current).toBe(service);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(cleanup).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
await service?.cleanup();
|
||||
await act(async () => {
|
||||
await promise;
|
||||
});
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { isFunction } from 'lodash';
|
||||
import useLatest from 'react-use/lib/useLatest';
|
||||
import type { DiscoverStateContainer } from '../application/main/state_management/discover_state';
|
||||
import type { CustomizationCallback } from './types';
|
||||
import type {
|
||||
|
@ -23,43 +22,6 @@ const customizationContext = createContext(createCustomizationService());
|
|||
|
||||
export const DiscoverCustomizationProvider = customizationContext.Provider;
|
||||
|
||||
export const useDiscoverCustomizationService = ({
|
||||
customizationCallbacks: originalCustomizationCallbacks,
|
||||
stateContainer,
|
||||
}: {
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
stateContainer?: DiscoverStateContainer;
|
||||
}) => {
|
||||
const customizationCallbacks = useLatest(originalCustomizationCallbacks);
|
||||
const [customizationService, setCustomizationService] = useState<DiscoverCustomizationService>();
|
||||
|
||||
useEffect(() => {
|
||||
setCustomizationService(undefined);
|
||||
|
||||
if (!stateContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customizations = createCustomizationService();
|
||||
const callbacks = customizationCallbacks.current.map((callback) =>
|
||||
Promise.resolve(callback({ customizations, stateContainer }))
|
||||
);
|
||||
const initialize = () => Promise.all(callbacks).then((result) => result.filter(isFunction));
|
||||
|
||||
initialize().then(() => {
|
||||
setCustomizationService(customizations);
|
||||
});
|
||||
|
||||
return () => {
|
||||
initialize().then((cleanups) => {
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
});
|
||||
};
|
||||
}, [customizationCallbacks, stateContainer]);
|
||||
|
||||
return customizationService;
|
||||
};
|
||||
|
||||
export const useDiscoverCustomization$ = <TCustomizationId extends DiscoverCustomizationId>(
|
||||
id: TCustomizationId
|
||||
) => useContext(customizationContext).get$(id);
|
||||
|
@ -70,3 +32,32 @@ export const useDiscoverCustomization = <TCustomizationId extends DiscoverCustom
|
|||
const customizationService = useContext(customizationContext);
|
||||
return useObservable(customizationService.get$(id), customizationService.get(id));
|
||||
};
|
||||
|
||||
export interface ConnectedCustomizationService extends DiscoverCustomizationService {
|
||||
cleanup: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const getConnectedCustomizationService = async ({
|
||||
customizationCallbacks,
|
||||
stateContainer,
|
||||
}: {
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}): Promise<ConnectedCustomizationService> => {
|
||||
const customizations = createCustomizationService();
|
||||
const callbacks = customizationCallbacks.map((callback) =>
|
||||
Promise.resolve(callback({ customizations, stateContainer }))
|
||||
);
|
||||
const initialize = () => Promise.all(callbacks).then((result) => result.filter(isFunction));
|
||||
|
||||
// TODO: Race condition?
|
||||
await initialize();
|
||||
|
||||
return {
|
||||
...customizations,
|
||||
cleanup: async () => {
|
||||
const cleanups = await initialize();
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue