mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Discover] Initial tabs implementation (disabled in main
) (#214861)
## Summary This PR restructures Discover's state management to support tabs as outlined in #215398, including the Redux store and `RuntimeStateManager`. It also adds the initial tabs implementation to the UI to start building on, but they're disabled by default with a hardcoded flag. Tabs can be enabled by setting `TABS_ENABLED = true` in `discover_main_route`, but they don't need to be thoroughly tested in this PR since most of the functionality is incomplete. There's also a flaw in the state management approach with `currentId` since depending on it can cause state to leak across tabs when switching tabs during async operations (e.g. data fetching). This shouldn't be an issue while tabs are disabled, and there will be a followup PR #215620 to address it. https://github.com/user-attachments/assets/ebbb9fa7-a3bc-4e82-9b5c-0d29cd0575f0 Part of #215398. ### 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 - [x] 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
2cd777d969
commit
0bb73eec2c
36 changed files with 775 additions and 314 deletions
|
@ -240,9 +240,10 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
|||
ControlGroupRendererApi | undefined
|
||||
>();
|
||||
const stateStorage = stateContainer.stateStorage;
|
||||
const currentTabId = stateContainer.internalState.getState().tabs.currentId;
|
||||
const dataView = useObservable(
|
||||
stateContainer.runtimeStateManager.currentDataView$,
|
||||
stateContainer.runtimeStateManager.currentDataView$.getValue()
|
||||
stateContainer.runtimeStateManager.tabs.byId[currentTabId].currentDataView$,
|
||||
stateContainer.runtimeStateManager.tabs.byId[currentTabId].currentDataView$.getValue()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,7 +16,6 @@ import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
|||
import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks';
|
||||
import {
|
||||
analyticsServiceMock,
|
||||
chromeServiceMock,
|
||||
coreMock,
|
||||
docLinksServiceMock,
|
||||
scopedHistoryMock,
|
||||
|
@ -150,18 +149,19 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
|
||||
corePluginMock.theme = theme;
|
||||
corePluginMock.chrome.getActiveSolutionNavId$.mockReturnValue(new BehaviorSubject(null));
|
||||
corePluginMock.chrome.getChromeStyle$.mockReturnValue(new BehaviorSubject('classic'));
|
||||
|
||||
return {
|
||||
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
|
||||
application: corePluginMock.application,
|
||||
core: corePluginMock,
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
chrome: chromeServiceMock.createStartContract(),
|
||||
chrome: corePluginMock.chrome,
|
||||
history: {
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
listen: jest.fn(),
|
||||
listen: jest.fn(() => () => {}),
|
||||
},
|
||||
getScopedHistory: () => scopedHistoryMock.create(),
|
||||
data: dataPlugin,
|
||||
|
|
|
@ -79,6 +79,7 @@ import {
|
|||
useInternalStateDispatch,
|
||||
useInternalStateSelector,
|
||||
} from '../../state_management/redux';
|
||||
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
|
||||
|
||||
const DiscoverGridMemoized = React.memo(DiscoverGrid);
|
||||
|
||||
|
@ -110,7 +111,7 @@ function DiscoverDocumentsComponent({
|
|||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services;
|
||||
const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
|
||||
const requestParams = useCurrentTabSelector((state) => state.dataRequestParams);
|
||||
const [
|
||||
dataSource,
|
||||
query,
|
||||
|
@ -153,7 +154,7 @@ function DiscoverDocumentsComponent({
|
|||
// 5. this is propagated to Discover's URL and causes an unwanted change of state to an unsorted state
|
||||
// This solution switches to the loading state in this component when the URL index doesn't match the dataView.id
|
||||
const isDataViewLoading =
|
||||
useInternalStateSelector((state) => state.isDataViewLoading) && !isEsqlMode;
|
||||
useCurrentTabSelector((state) => state.isDataViewLoading) && !isEsqlMode;
|
||||
const isEmptyDataResult =
|
||||
isEsqlMode || !documentState.result || documentState.result.length === 0;
|
||||
const rows = useMemo(() => documentState.result || [], [documentState.result]);
|
||||
|
|
|
@ -57,7 +57,8 @@ import type { PanelsToggleProps } from '../../../../components/panels_toggle';
|
|||
import { PanelsToggle } from '../../../../components/panels_toggle';
|
||||
import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
import { useCurrentDataView, useInternalStateSelector } from '../../state_management/redux';
|
||||
import { useCurrentDataView } from '../../state_management/redux';
|
||||
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
|
||||
|
||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
|
@ -102,7 +103,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
|
||||
});
|
||||
const dataView = useCurrentDataView();
|
||||
const dataViewLoading = useInternalStateSelector((state) => state.isDataViewLoading);
|
||||
const dataViewLoading = useCurrentTabSelector((state) => state.isDataViewLoading);
|
||||
const dataState: DataMainMsg = useDataState(main$);
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const fetchCounter = useRef<number>(0);
|
||||
|
|
|
@ -58,8 +58,8 @@ import {
|
|||
internalStateActions,
|
||||
useCurrentDataView,
|
||||
useInternalStateDispatch,
|
||||
useInternalStateSelector,
|
||||
} from '../../state_management/redux';
|
||||
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
|
||||
|
||||
const EMPTY_ESQL_COLUMNS: DatatableColumn[] = [];
|
||||
const EMPTY_FILTERS: Filter[] = [];
|
||||
|
@ -227,7 +227,7 @@ export const useDiscoverHistogram = ({
|
|||
* Request params
|
||||
*/
|
||||
const { query, filters } = useQuerySubscriber({ data: services.data });
|
||||
const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
|
||||
const requestParams = useCurrentTabSelector((state) => state.dataRequestParams);
|
||||
const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams;
|
||||
// When in ES|QL mode, update the data view, query, and
|
||||
// columns only when documents are done fetching so the Lens suggestions
|
||||
|
|
|
@ -9,4 +9,8 @@
|
|||
|
||||
export { BrandedLoadingIndicator } from './branded_loading_indicator';
|
||||
export { NoDataPage } from './no_data_page';
|
||||
export { DiscoverSessionView } from './session_view';
|
||||
export {
|
||||
DiscoverSessionView,
|
||||
type DiscoverSessionViewProps,
|
||||
type DiscoverSessionViewRef,
|
||||
} from './session_view';
|
||||
|
|
|
@ -55,10 +55,10 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
* Start state syncing and fetch data if necessary
|
||||
*/
|
||||
useEffect(() => {
|
||||
const unsubscribe = stateContainer.actions.initializeAndSync();
|
||||
stateContainer.actions.initializeAndSync();
|
||||
addLog('[DiscoverMainApp] state container initialization triggers data fetching');
|
||||
stateContainer.actions.fetchData(true);
|
||||
return () => unsubscribe();
|
||||
return () => stateContainer.actions.stopSyncing();
|
||||
}, [stateContainer]);
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle } 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';
|
||||
|
@ -28,6 +28,7 @@ import {
|
|||
useInternalStateDispatch,
|
||||
useInternalStateSelector,
|
||||
useRuntimeState,
|
||||
useCurrentTabRuntimeState,
|
||||
} from '../../state_management/redux';
|
||||
import type {
|
||||
CustomizationCallback,
|
||||
|
@ -46,7 +47,7 @@ import { RedirectWhenSavedObjectNotFound } from './redirect_not_found';
|
|||
import { DiscoverMainApp } from './main_app';
|
||||
import { useAsyncFunction } from '../../hooks/use_async_function';
|
||||
|
||||
interface DiscoverSessionViewProps {
|
||||
export interface DiscoverSessionViewProps {
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
urlStateStorage: IKbnUrlStateStorage;
|
||||
|
@ -54,6 +55,10 @@ interface DiscoverSessionViewProps {
|
|||
runtimeStateManager: RuntimeStateManager;
|
||||
}
|
||||
|
||||
export interface DiscoverSessionViewRef {
|
||||
stopSyncing: () => void;
|
||||
}
|
||||
|
||||
type SessionInitializationState =
|
||||
| {
|
||||
showNoDataPage: true;
|
||||
|
@ -69,126 +74,147 @@ type InitializeSession = (options?: {
|
|||
defaultUrlState?: DiscoverAppState;
|
||||
}) => Promise<SessionInitializationState>;
|
||||
|
||||
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 [initializeSessionState, initializeSession] = useAsyncFunction<InitializeSession>(
|
||||
async ({ dataViewSpec, defaultUrlState } = {}) => {
|
||||
const stateContainer = getDiscoverStateContainer({
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer: urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
});
|
||||
const { showNoDataPage } = await dispatch(
|
||||
internalStateActions.initializeSession({
|
||||
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,
|
||||
});
|
||||
});
|
||||
const customizationService = useDiscoverCustomizationService({
|
||||
customizationCallbacks,
|
||||
stateContainer: initializeSessionState.value?.stateContainer,
|
||||
});
|
||||
const initializationState = useInternalStateSelector((state) => state.initializationState);
|
||||
const currentDataView = useRuntimeState(runtimeStateManager.currentDataView$);
|
||||
const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
}, [discoverSessionId, initializeSessionWithDefaultLocationState]);
|
||||
|
||||
useUrl({
|
||||
history,
|
||||
savedSearchId: discoverSessionId,
|
||||
onNewUrl: () => {
|
||||
initializeSessionWithDefaultLocationState.current();
|
||||
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 [initializeSessionState, initializeSession] = useAsyncFunction<InitializeSession>(
|
||||
async ({ dataViewSpec, defaultUrlState } = {}) => {
|
||||
initializeSessionState.value?.stateContainer?.actions.stopSyncing();
|
||||
|
||||
useAlertResultsToast();
|
||||
const stateContainer = getDiscoverStateContainer({
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer: urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
});
|
||||
const { showNoDataPage } = await dispatch(
|
||||
internalStateActions.initializeSession({
|
||||
stateContainer,
|
||||
discoverSessionId,
|
||||
dataViewSpec,
|
||||
defaultUrlState,
|
||||
})
|
||||
);
|
||||
|
||||
useExecutionContext(core.executionContext, {
|
||||
type: 'application',
|
||||
page: 'app',
|
||||
id: discoverSessionId || 'new',
|
||||
});
|
||||
return showNoDataPage ? { showNoDataPage } : { showNoDataPage, stateContainer };
|
||||
}
|
||||
);
|
||||
const initializeSessionWithDefaultLocationState = useLatest(() => {
|
||||
const historyLocationState = getScopedHistory<
|
||||
MainHistoryLocationState & { defaultState?: DiscoverAppState }
|
||||
>()?.location.state;
|
||||
initializeSession({
|
||||
dataViewSpec: historyLocationState?.dataViewSpec,
|
||||
defaultUrlState: historyLocationState?.defaultState,
|
||||
});
|
||||
});
|
||||
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$);
|
||||
|
||||
if (initializeSessionState.loading) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
stopSyncing: () => initializeSessionState.value?.stateContainer?.actions.stopSyncing(),
|
||||
}),
|
||||
[initializeSessionState.value?.stateContainer]
|
||||
);
|
||||
|
||||
if (initializeSessionState.error) {
|
||||
if (initializeSessionState.error instanceof SavedObjectNotFound) {
|
||||
useEffect(() => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <DiscoverError error={initializeSessionState.error} />;
|
||||
}
|
||||
|
||||
if (initializeSessionState.value.showNoDataPage) {
|
||||
return (
|
||||
<RedirectWhenSavedObjectNotFound
|
||||
error={initializeSessionState.error}
|
||||
discoverSessionId={discoverSessionId}
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <DiscoverError error={initializeSessionState.error} />;
|
||||
}
|
||||
if (!customizationService || !currentDataView) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (initializeSessionState.value.showNoDataPage) {
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<DiscoverCustomizationProvider value={customizationService}>
|
||||
<DiscoverMainProvider value={initializeSessionState.value.stateContainer}>
|
||||
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
|
||||
<DiscoverMainApp stateContainer={initializeSessionState.value.stateContainer} />
|
||||
</RuntimeStateProvider>
|
||||
</DiscoverMainProvider>
|
||||
</DiscoverCustomizationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customizationService || !currentDataView) {
|
||||
return <BrandedLoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscoverCustomizationProvider value={customizationService}>
|
||||
<DiscoverMainProvider value={initializeSessionState.value.stateContainer}>
|
||||
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
|
||||
<DiscoverMainApp stateContainer={initializeSessionState.value.stateContainer} />
|
||||
</RuntimeStateProvider>
|
||||
</DiscoverMainProvider>
|
||||
</DiscoverCustomizationProvider>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
export { TabsView } from './tabs_view';
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { type TabItem, UnifiedTabs, TabStatus } from '@kbn/unified-tabs';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import type { DiscoverSessionViewRef } from '../session_view';
|
||||
import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view';
|
||||
import {
|
||||
createTabItem,
|
||||
internalStateActions,
|
||||
selectAllTabs,
|
||||
selectCurrentTab,
|
||||
useInternalStateDispatch,
|
||||
useInternalStateSelector,
|
||||
} from '../../state_management/redux';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
|
||||
export const TabsView = ({ sessionViewProps }: { sessionViewProps: DiscoverSessionViewProps }) => {
|
||||
const services = useDiscoverServices();
|
||||
const dispatch = useInternalStateDispatch();
|
||||
const currentTab = useInternalStateSelector(selectCurrentTab);
|
||||
const allTabs = useInternalStateSelector(selectAllTabs);
|
||||
const [initialItems] = useState<TabItem[]>(() => allTabs.map((tab) => pick(tab, 'id', 'label')));
|
||||
const sessionViewRef = useRef<DiscoverSessionViewRef>(null);
|
||||
|
||||
return (
|
||||
<UnifiedTabs
|
||||
services={services}
|
||||
initialItems={initialItems}
|
||||
onChanged={(updateState) =>
|
||||
dispatch(
|
||||
internalStateActions.updateTabs({
|
||||
updateState,
|
||||
stopSyncing: sessionViewRef.current?.stopSyncing,
|
||||
})
|
||||
)
|
||||
}
|
||||
createItem={() => createTabItem(allTabs)}
|
||||
getPreviewData={() => ({
|
||||
query: { language: 'kuery', query: 'sample query' },
|
||||
status: TabStatus.SUCCESS,
|
||||
})}
|
||||
renderContent={() => (
|
||||
<DiscoverSessionView key={currentTab.id} ref={sessionViewRef} {...sessionViewProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -17,7 +17,7 @@ import type { SavedSearch, SaveSavedSearchOptions } from '@kbn/saved-search-plug
|
|||
import type { DiscoverServices } from '../../../../build_services';
|
||||
import type { DiscoverStateContainer } from '../../state_management/discover_state';
|
||||
import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size';
|
||||
import { internalStateActions } from '../../state_management/redux';
|
||||
import { internalStateActions, selectCurrentTab } from '../../state_management/redux';
|
||||
|
||||
async function saveDataSource({
|
||||
savedSearch,
|
||||
|
@ -95,8 +95,8 @@ export async function onSaveSearch({
|
|||
}) {
|
||||
const { uiSettings, savedObjectsTagging } = services;
|
||||
const dataView = savedSearch.searchSource.getField('index');
|
||||
const overriddenVisContextAfterInvalidation =
|
||||
state.internalState.getState().overriddenVisContextAfterInvalidation;
|
||||
const currentTab = selectCurrentTab(state.internalState.getState());
|
||||
const overriddenVisContextAfterInvalidation = currentTab.overriddenVisContextAfterInvalidation;
|
||||
|
||||
const onSave = async ({
|
||||
newTitle,
|
||||
|
|
|
@ -35,7 +35,7 @@ import type {
|
|||
} from '../state_management/discover_data_state_container';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
import { fetchEsql } from './fetch_esql';
|
||||
import type { InternalStateStore } from '../state_management/redux';
|
||||
import { selectCurrentTab, type InternalStateStore } from '../state_management/redux';
|
||||
|
||||
export interface FetchDeps {
|
||||
abortController: AbortController;
|
||||
|
@ -78,6 +78,7 @@ export function fetchAll(
|
|||
const query = getAppState().query;
|
||||
const prevQuery = dataSubjects.documents$.getValue().query;
|
||||
const isEsqlQuery = isOfAggregateQueryType(query);
|
||||
const currentTab = selectCurrentTab(internalState.getState());
|
||||
|
||||
if (reset) {
|
||||
sendResetMsg(dataSubjects, initialFetchStatus);
|
||||
|
@ -89,7 +90,7 @@ export function fetchAll(
|
|||
dataView,
|
||||
services,
|
||||
sort: getAppState().sort as SortOrder[],
|
||||
inputTimeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
|
||||
inputTimeRange: currentTab.dataRequestParams.timeRangeAbsolute,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,7 +111,7 @@ export function fetchAll(
|
|||
data,
|
||||
expressions,
|
||||
profilesManager,
|
||||
timeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
|
||||
timeRange: currentTab.dataRequestParams.timeRangeAbsolute,
|
||||
})
|
||||
: fetchDocuments(searchSource, fetchDeps);
|
||||
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
|
||||
|
|
|
@ -24,12 +24,17 @@ import {
|
|||
import type { RootProfileState } from '../../context_awareness';
|
||||
import { useRootProfile, useDefaultAdHocDataViews } from '../../context_awareness';
|
||||
import { DiscoverError } from '../../components/common/error_alert';
|
||||
import type { DiscoverSessionViewProps } from './components/session_view';
|
||||
import {
|
||||
BrandedLoadingIndicator,
|
||||
DiscoverSessionView,
|
||||
NoDataPage,
|
||||
} from './components/session_view';
|
||||
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 interface MainRouteProps {
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
|
@ -120,16 +125,22 @@ export const DiscoverMainRoute = ({
|
|||
);
|
||||
}
|
||||
|
||||
const sessionViewProps: DiscoverSessionViewProps = {
|
||||
customizationContext,
|
||||
customizationCallbacks,
|
||||
urlStateStorage,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
};
|
||||
|
||||
return (
|
||||
<InternalStateProvider store={internalState}>
|
||||
<rootProfileState.AppWrapper>
|
||||
<DiscoverSessionView
|
||||
customizationContext={customizationContext}
|
||||
customizationCallbacks={customizationCallbacks}
|
||||
urlStateStorage={urlStateStorage}
|
||||
internalState={internalState}
|
||||
runtimeStateManager={runtimeStateManager}
|
||||
/>
|
||||
{TABS_ENABLED ? (
|
||||
<TabsView sessionViewProps={sessionViewProps} />
|
||||
) : (
|
||||
<DiscoverSessionView {...sessionViewProps} />
|
||||
)}
|
||||
</rootProfileState.AppWrapper>
|
||||
</InternalStateProvider>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ import { dataViewAdHoc } from '../../../__mocks__/data_view_complex';
|
|||
import type { EsHitRecord } from '@kbn/discover-utils';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { omit } from 'lodash';
|
||||
import { internalStateActions } from '../state_management/redux';
|
||||
import { internalStateActions, selectCurrentTab } from '../state_management/redux';
|
||||
|
||||
async function getHookProps(
|
||||
query: AggregateQuery | Query | undefined,
|
||||
|
@ -507,7 +507,10 @@ describe('useEsqlMode', () => {
|
|||
);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -524,7 +527,10 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
|
@ -549,7 +555,10 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -567,7 +576,10 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
|
@ -586,7 +598,10 @@ describe('useEsqlMode', () => {
|
|||
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
|
||||
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -599,7 +614,10 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -613,7 +631,10 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: true,
|
||||
rowHeight: false,
|
||||
|
|
|
@ -23,7 +23,7 @@ import { getSavedSearchContainer } from './discover_saved_search_container';
|
|||
import { getDiscoverGlobalStateContainer } from './discover_global_state_container';
|
||||
import { omit } from 'lodash';
|
||||
import type { InternalStateStore } from './redux';
|
||||
import { createInternalStateStore, createRuntimeStateManager } from './redux';
|
||||
import { createInternalStateStore, createRuntimeStateManager, selectCurrentTab } from './redux';
|
||||
import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context';
|
||||
|
||||
let history: History;
|
||||
|
@ -274,13 +274,17 @@ describe('Test discover app state container', () => {
|
|||
describe('initAndSync', () => {
|
||||
it('should call setResetDefaultProfileState correctly with no initial state', () => {
|
||||
const state = getStateContainer();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
});
|
||||
state.initAndSync();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
breakdownField: true,
|
||||
|
@ -291,13 +295,17 @@ describe('Test discover app state container', () => {
|
|||
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
|
||||
stateStorageGetSpy.mockReturnValue({ columns: ['test'] });
|
||||
const state = getStateContainer();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
});
|
||||
state.initAndSync();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: true,
|
||||
breakdownField: true,
|
||||
|
@ -308,13 +316,17 @@ describe('Test discover app state container', () => {
|
|||
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
|
||||
stateStorageGetSpy.mockReturnValue({ rowHeight: 5 });
|
||||
const state = getStateContainer();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
});
|
||||
state.initAndSync();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: true,
|
||||
rowHeight: false,
|
||||
breakdownField: true,
|
||||
|
@ -331,13 +343,17 @@ describe('Test discover app state container', () => {
|
|||
managed: false,
|
||||
});
|
||||
const state = getStateContainer();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
});
|
||||
state.initAndSync();
|
||||
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
|
||||
expect(
|
||||
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
|
|
|
@ -17,7 +17,7 @@ import type { DataDocuments$ } from './discover_data_state_container';
|
|||
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
|
||||
import { fetchDocuments } from '../data_fetching/fetch_documents';
|
||||
import { omit } from 'lodash';
|
||||
import { internalStateActions } from './redux';
|
||||
import { internalStateActions, selectCurrentTab } from './redux';
|
||||
|
||||
jest.mock('../data_fetching/fetch_documents', () => ({
|
||||
fetchDocuments: jest.fn().mockResolvedValue({ records: [] }),
|
||||
|
@ -195,7 +195,10 @@ describe('test getDataStateContainer', () => {
|
|||
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
});
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -230,7 +233,10 @@ describe('test getDataStateContainer', () => {
|
|||
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
});
|
||||
expect(
|
||||
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
|
||||
omit(
|
||||
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
|
||||
'resetId'
|
||||
)
|
||||
).toEqual({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
|
|
@ -31,7 +31,7 @@ import { sendResetMsg } from '../hooks/use_saved_search_messages';
|
|||
import { getFetch$ } from '../data_fetching/get_fetch_observable';
|
||||
import { getDefaultProfileState } from './utils/get_default_profile_state';
|
||||
import type { InternalStateStore, RuntimeStateManager } from './redux';
|
||||
import { internalStateActions } from './redux';
|
||||
import { internalStateActions, selectCurrentTab, selectCurrentTabRuntimeState } from './redux';
|
||||
|
||||
export interface SavedSearchData {
|
||||
main$: DataMain$;
|
||||
|
@ -263,8 +263,13 @@ export function getDataStateContainer({
|
|||
query: appStateContainer.getState().query,
|
||||
});
|
||||
|
||||
const { resetDefaultProfileState } = internalState.getState();
|
||||
const dataView = runtimeStateManager.currentDataView$.getValue();
|
||||
const currentInternalState = internalState.getState();
|
||||
const { resetDefaultProfileState } = selectCurrentTab(currentInternalState);
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(
|
||||
currentInternalState,
|
||||
runtimeStateManager
|
||||
);
|
||||
const dataView = currentDataView$.getValue();
|
||||
const defaultProfileState = dataView
|
||||
? getDefaultProfileState({ profilesManager, resetDefaultProfileState, dataView })
|
||||
: undefined;
|
||||
|
@ -293,7 +298,7 @@ export function getDataStateContainer({
|
|||
},
|
||||
async () => {
|
||||
const { resetDefaultProfileState: currentResetDefaultProfileState } =
|
||||
internalState.getState();
|
||||
selectCurrentTab(internalState.getState());
|
||||
|
||||
if (currentResetDefaultProfileState.resetId !== resetDefaultProfileState.resetId) {
|
||||
return;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import type { DiscoverStateContainer } from './discover_state';
|
||||
import { createSearchSessionRestorationDataProvider } from './discover_state';
|
||||
import { internalStateActions } from './redux';
|
||||
import { internalStateActions, selectCurrentTab, selectCurrentTabRuntimeState } from './redux';
|
||||
import type { History } from 'history';
|
||||
import { createBrowserHistory, createMemoryHistory } from 'history';
|
||||
import { createSearchSourceMock, dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
|
@ -249,9 +249,9 @@ describe('Discover state', () => {
|
|||
} as SavedSearch;
|
||||
|
||||
const { state } = await getState('/#?_a=(sort:!(!(timestamp,desc)))', { savedSearch });
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
expect(state.appState.getState().sort).toEqual([['timestamp', 'desc']]);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('Empty URL should use saved search sort for state', async () => {
|
||||
|
@ -269,9 +269,9 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
expect(state.appState.getState().sort).toEqual([['bytes', 'desc']]);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -442,10 +442,20 @@ describe('Discover state', () => {
|
|||
|
||||
test('setDataView', async () => {
|
||||
const { state, runtimeStateManager } = await getState('');
|
||||
expect(runtimeStateManager.currentDataView$.getValue()).toBeUndefined();
|
||||
expect(
|
||||
selectCurrentTabRuntimeState(
|
||||
state.internalState.getState(),
|
||||
runtimeStateManager
|
||||
).currentDataView$.getValue()
|
||||
).toBeUndefined();
|
||||
state.actions.setDataView(dataViewMock);
|
||||
expect(runtimeStateManager.currentDataView$.getValue()).toBe(dataViewMock);
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id);
|
||||
expect(
|
||||
selectCurrentTabRuntimeState(
|
||||
state.internalState.getState(),
|
||||
runtimeStateManager
|
||||
).currentDataView$.getValue()
|
||||
).toBe(dataViewMock);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id);
|
||||
});
|
||||
|
||||
test('fetchData', async () => {
|
||||
|
@ -462,12 +472,12 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
state.actions.fetchData();
|
||||
await waitFor(() => {
|
||||
expect(dataState.data$.documents$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
});
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
|
||||
expect(dataState.data$.totalHits$.value.result).toBe(0);
|
||||
expect(dataState.data$.documents$.value.result).toEqual([]);
|
||||
|
@ -492,7 +502,7 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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:!())"`
|
||||
|
@ -517,7 +527,7 @@ describe('Discover state', () => {
|
|||
}
|
||||
`);
|
||||
expect(searchSource.getField('index')?.id).toEqual('the-data-view-id');
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadNewSavedSearch given an empty URL using loadSavedSearch', async () => {
|
||||
|
@ -533,13 +543,13 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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:!())"`
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadNewSavedSearch with URL changing interval state', async () => {
|
||||
|
@ -558,13 +568,13 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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=()"`
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch with no id, given URL changes state', async () => {
|
||||
|
@ -583,13 +593,13 @@ describe('Discover state', () => {
|
|||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
expect(newSavedSearch?.id).toBeUndefined();
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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=()"`
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch given an empty URL, no state changes', async () => {
|
||||
|
@ -618,14 +628,14 @@ describe('Discover state', () => {
|
|||
})
|
||||
);
|
||||
const newSavedSearch = state.savedSearchState.getState();
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(newSavedSearch?.id).toBe('the-saved-search-id');
|
||||
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:!())"`
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch given a URL with different interval and columns modifying the state', async () => {
|
||||
|
@ -643,13 +653,13 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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=()"`
|
||||
);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch given a URL with different time range than the stored one showing as changed', async () => {
|
||||
|
@ -673,10 +683,10 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch given a URL with different refresh interval than the stored one showing as changed', async () => {
|
||||
|
@ -704,10 +714,10 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch given a URL with matching time range and refresh interval not showing as changed', async () => {
|
||||
|
@ -735,10 +745,10 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('loadSavedSearch ignoring hideChart in URL', async () => {
|
||||
|
@ -1057,7 +1067,7 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = actions.initializeAndSync();
|
||||
actions.initializeAndSync();
|
||||
await new Promise(process.nextTick);
|
||||
// test initial state
|
||||
expect(dataState.fetch).toHaveBeenCalledTimes(0);
|
||||
|
@ -1078,7 +1088,7 @@ describe('Discover state', () => {
|
|||
);
|
||||
// check if the changed data view is reflected in the URL
|
||||
expect(getCurrentUrl()).toContain(dataViewComplexMock.id);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onDataViewCreated - persisted data view', async () => {
|
||||
|
@ -1092,10 +1102,12 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.onDataViewCreated(dataViewComplexMock);
|
||||
await waitFor(() => {
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewComplexMock.id);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
|
||||
dataViewComplexMock.id
|
||||
);
|
||||
});
|
||||
expect(state.appState.getState().dataSource).toEqual(
|
||||
createDataViewDataSource({ dataViewId: dataViewComplexMock.id! })
|
||||
|
@ -1103,7 +1115,7 @@ describe('Discover state', () => {
|
|||
expect(state.savedSearchState.getState().searchSource.getField('index')!.id).toBe(
|
||||
dataViewComplexMock.id
|
||||
);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onDataViewCreated - ad-hoc data view', async () => {
|
||||
|
@ -1117,7 +1129,7 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
jest
|
||||
.spyOn(discoverServiceMock.dataViews, 'get')
|
||||
.mockImplementationOnce((id) =>
|
||||
|
@ -1125,7 +1137,7 @@ describe('Discover state', () => {
|
|||
);
|
||||
await state.actions.onDataViewCreated(dataViewAdHoc);
|
||||
await waitFor(() => {
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewAdHoc.id);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewAdHoc.id);
|
||||
});
|
||||
expect(state.appState.getState().dataSource).toEqual(
|
||||
createDataViewDataSource({ dataViewId: dataViewAdHoc.id! })
|
||||
|
@ -1133,7 +1145,7 @@ describe('Discover state', () => {
|
|||
expect(state.savedSearchState.getState().searchSource.getField('index')!.id).toBe(
|
||||
dataViewAdHoc.id
|
||||
);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onDataViewEdited - persisted data view', async () => {
|
||||
|
@ -1147,31 +1159,33 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const selectedDataViewId = state.internalState.getState().dataViewId;
|
||||
const selectedDataViewId = selectCurrentTab(state.internalState.getState()).dataViewId;
|
||||
expect(selectedDataViewId).toBe(dataViewMock.id);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.onDataViewEdited(dataViewMock);
|
||||
await waitFor(() => {
|
||||
expect(state.internalState.getState().dataViewId).toBe(selectedDataViewId);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
|
||||
selectedDataViewId
|
||||
);
|
||||
});
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onDataViewEdited - ad-hoc data view', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.onDataViewCreated(dataViewAdHoc);
|
||||
const previousId = dataViewAdHoc.id;
|
||||
await state.actions.onDataViewEdited(dataViewAdHoc);
|
||||
await waitFor(() => {
|
||||
expect(state.internalState.getState().dataViewId).not.toBe(previousId);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).not.toBe(previousId);
|
||||
});
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onOpenSavedSearch - same target id', async () => {
|
||||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await state.internalState.dispatch(
|
||||
internalStateActions.initializeSession({
|
||||
stateContainer: state,
|
||||
|
@ -1185,7 +1199,7 @@ describe('Discover state', () => {
|
|||
expect(state.savedSearchState.getState().hideChart).toBe(true);
|
||||
state.actions.onOpenSavedSearch(savedSearchMock.id!);
|
||||
expect(state.savedSearchState.getState().hideChart).toBe(undefined);
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('onOpenSavedSearch - cleanup of previous filter', async () => {
|
||||
|
@ -1227,13 +1241,13 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
state.actions.initializeAndSync();
|
||||
await state.actions.createAndAppendAdHocDataView({ title: 'ad-hoc-test' });
|
||||
expect(state.appState.getState().dataSource).toEqual(
|
||||
createDataViewDataSource({ dataViewId: 'ad-hoc-id' })
|
||||
);
|
||||
expect(state.runtimeStateManager.adHocDataViews$.getValue()[0].id).toBe('ad-hoc-id');
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('undoSavedSearchChanges - when changing data views', async () => {
|
||||
|
@ -1248,12 +1262,12 @@ describe('Discover state', () => {
|
|||
defaultUrlState: undefined,
|
||||
})
|
||||
);
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
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:!())';
|
||||
expect(getCurrentUrl()).toBe(initialUrlState);
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id!);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id!);
|
||||
|
||||
// Change the data view, this should change the URL and trigger a fetch
|
||||
await state.actions.onChangeDataView(dataViewComplexMock.id!);
|
||||
|
@ -1264,7 +1278,9 @@ describe('Discover state', () => {
|
|||
await waitFor(() => {
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewComplexMock.id!);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
|
||||
dataViewComplexMock.id!
|
||||
);
|
||||
|
||||
// Undo all changes to the saved search, this should trigger a fetch, again
|
||||
await state.actions.undoSavedSearchChanges();
|
||||
|
@ -1273,9 +1289,9 @@ describe('Discover state', () => {
|
|||
await waitFor(() => {
|
||||
expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id!);
|
||||
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id!);
|
||||
|
||||
unsubscribe();
|
||||
state.actions.stopSyncing();
|
||||
});
|
||||
|
||||
test('undoSavedSearchChanges with timeRestore', async () => {
|
||||
|
|
|
@ -44,7 +44,7 @@ import {
|
|||
isDataSourceType,
|
||||
} from '../../../../common/data_sources';
|
||||
import type { InternalStateStore, RuntimeStateManager } from './redux';
|
||||
import { internalStateActions } from './redux';
|
||||
import { internalStateActions, selectCurrentTabRuntimeState } from './redux';
|
||||
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
|
||||
import { getSavedSearchContainer } from './discover_saved_search_container';
|
||||
|
||||
|
@ -144,7 +144,11 @@ export interface DiscoverStateContainer {
|
|||
/**
|
||||
* Initializing state containers and start subscribing to changes triggering e.g. data fetching
|
||||
*/
|
||||
initializeAndSync: () => () => void;
|
||||
initializeAndSync: () => void;
|
||||
/**
|
||||
* Stop syncing the state containers started by initializeAndSync
|
||||
*/
|
||||
stopSyncing: () => void;
|
||||
/**
|
||||
* Create and select a temporary/adhoc data view by a given index pattern
|
||||
* Used by the Data View Picker
|
||||
|
@ -302,7 +306,11 @@ export function getDiscoverStateContainer({
|
|||
* This is to prevent duplicate ids messing with our system
|
||||
*/
|
||||
const updateAdHocDataViewId = async () => {
|
||||
const prevDataView = runtimeStateManager.currentDataView$.getValue();
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(
|
||||
internalState.getState(),
|
||||
runtimeStateManager
|
||||
);
|
||||
const prevDataView = currentDataView$.getValue();
|
||||
if (!prevDataView || prevDataView.isPersisted()) return;
|
||||
|
||||
const nextDataView = await services.dataViews.create({
|
||||
|
@ -408,6 +416,13 @@ export function getDiscoverStateContainer({
|
|||
fetchData();
|
||||
};
|
||||
|
||||
let internalStopSyncing = () => {};
|
||||
|
||||
const stopSyncing = () => {
|
||||
internalStopSyncing();
|
||||
internalStopSyncing = () => {};
|
||||
};
|
||||
|
||||
/**
|
||||
* state containers initializing and subscribing to changes triggering e.g. data fetching
|
||||
*/
|
||||
|
@ -442,8 +457,12 @@ export function getDiscoverStateContainer({
|
|||
|
||||
// updates saved search when query or filters change, triggers data fetching
|
||||
const filterUnsubscribe = merge(services.filterManager.getFetches$()).subscribe(() => {
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(
|
||||
internalState.getState(),
|
||||
runtimeStateManager
|
||||
);
|
||||
savedSearchContainer.update({
|
||||
nextDataView: runtimeStateManager.currentDataView$.getValue(),
|
||||
nextDataView: currentDataView$.getValue(),
|
||||
nextState: appStateContainer.getState(),
|
||||
useFilterAndQueryServices: true,
|
||||
});
|
||||
|
@ -468,7 +487,7 @@ export function getDiscoverStateContainer({
|
|||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
internalStopSyncing = () => {
|
||||
unsubscribeData();
|
||||
appStateUnsubscribe();
|
||||
appStateInitAndSyncUnsubscribe();
|
||||
|
@ -581,6 +600,7 @@ export function getDiscoverStateContainer({
|
|||
customizationContext,
|
||||
actions: {
|
||||
initializeAndSync,
|
||||
stopSyncing,
|
||||
fetchData,
|
||||
onChangeDataView,
|
||||
createAndAppendAdHocDataView,
|
||||
|
|
|
@ -10,12 +10,20 @@
|
|||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { differenceBy } from 'lodash';
|
||||
import { internalStateSlice, type InternalStateThunkActionCreator } from '../internal_state';
|
||||
import { createInternalStateAsyncThunk } from '../utils';
|
||||
import { selectCurrentTabRuntimeState } from '../runtime_state';
|
||||
|
||||
export const loadDataViewList = createInternalStateAsyncThunk(
|
||||
'internalState/loadDataViewList',
|
||||
async (_, { extra: { services } }) => services.dataViews.getIdsWithTitle(true)
|
||||
);
|
||||
|
||||
export const setDataView: InternalStateThunkActionCreator<[DataView]> =
|
||||
(dataView) =>
|
||||
(dispatch, _, { runtimeStateManager }) => {
|
||||
(dispatch, getState, { runtimeStateManager }) => {
|
||||
dispatch(internalStateSlice.actions.setDataViewId(dataView.id));
|
||||
runtimeStateManager.currentDataView$.next(dataView);
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(getState(), runtimeStateManager);
|
||||
currentDataView$.next(dataView);
|
||||
};
|
||||
|
||||
export const setAdHocDataViews: InternalStateThunkActionCreator<[DataView[]]> =
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
|
||||
export * from './data_views';
|
||||
export * from './initialize_session';
|
||||
export * from './tabs';
|
||||
|
|
|
@ -30,6 +30,7 @@ import { isRefreshIntervalValid, isTimeRangeValid } from '../../../../../utils/v
|
|||
import { getValidFilters } from '../../../../../utils/get_valid_filters';
|
||||
import { updateSavedSearch } from '../../utils/update_saved_search';
|
||||
import { APP_STATE_URL_KEY } from '../../../../../../common';
|
||||
import { selectCurrentTabRuntimeState } from '../runtime_state';
|
||||
|
||||
export interface InitializeSessionParams {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
|
@ -108,10 +109,12 @@ export const initializeSession: InternalStateThunkActionCreator<
|
|||
let dataView: DataView;
|
||||
|
||||
if (isOfAggregateQueryType(initialQuery)) {
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(getState(), runtimeStateManager);
|
||||
|
||||
// Regardless of what was requested, we always use ad hoc data views for ES|QL
|
||||
dataView = await getEsqlDataView(
|
||||
initialQuery,
|
||||
discoverSessionDataView ?? runtimeStateManager.currentDataView$.getValue(),
|
||||
discoverSessionDataView ?? currentDataView$.getValue(),
|
||||
services
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 type { TabbedContentState } from '@kbn/unified-tabs/src/components/tabbed_content/tabbed_content';
|
||||
import { differenceBy } from 'lodash';
|
||||
import type { TabState } from '../types';
|
||||
import { selectAllTabs, selectCurrentTab } from '../selectors';
|
||||
import {
|
||||
defaultTabState,
|
||||
internalStateSlice,
|
||||
type InternalStateThunkActionCreator,
|
||||
} from '../internal_state';
|
||||
import { createTabRuntimeState } from '../runtime_state';
|
||||
|
||||
export const setTabs: InternalStateThunkActionCreator<
|
||||
[Parameters<typeof internalStateSlice.actions.setTabs>[0]]
|
||||
> =
|
||||
(params) =>
|
||||
(dispatch, getState, { runtimeStateManager }) => {
|
||||
const previousTabs = selectAllTabs(getState());
|
||||
const removedTabs = differenceBy(previousTabs, params.allTabs, (tab) => tab.id);
|
||||
const addedTabs = differenceBy(params.allTabs, previousTabs, (tab) => tab.id);
|
||||
|
||||
for (const tab of removedTabs) {
|
||||
delete runtimeStateManager.tabs.byId[tab.id];
|
||||
}
|
||||
|
||||
for (const tab of addedTabs) {
|
||||
runtimeStateManager.tabs.byId[tab.id] = createTabRuntimeState();
|
||||
}
|
||||
|
||||
dispatch(internalStateSlice.actions.setTabs(params));
|
||||
};
|
||||
|
||||
export interface UpdateTabsParams {
|
||||
updateState: TabbedContentState;
|
||||
stopSyncing?: () => void;
|
||||
}
|
||||
|
||||
export const updateTabs: InternalStateThunkActionCreator<[UpdateTabsParams], Promise<void>> =
|
||||
({ updateState: { items, selectedItem }, stopSyncing }) =>
|
||||
async (dispatch, getState, { urlStateStorage }) => {
|
||||
const currentState = getState();
|
||||
const currentTab = selectCurrentTab(currentState);
|
||||
let updatedTabs = items.map<TabState>((item) => {
|
||||
const existingTab = currentState.tabs.byId[item.id];
|
||||
return existingTab ? { ...existingTab, ...item } : { ...defaultTabState, ...item };
|
||||
});
|
||||
|
||||
if (selectedItem?.id !== currentTab.id) {
|
||||
stopSyncing?.();
|
||||
|
||||
updatedTabs = updatedTabs.map((tab) =>
|
||||
tab.id === currentTab.id
|
||||
? {
|
||||
...tab,
|
||||
globalState: urlStateStorage.get('_g') ?? undefined,
|
||||
appState: urlStateStorage.get('_a') ?? undefined,
|
||||
}
|
||||
: tab
|
||||
);
|
||||
|
||||
const existingTab = selectedItem ? currentState.tabs.byId[selectedItem.id] : undefined;
|
||||
|
||||
if (existingTab) {
|
||||
await urlStateStorage.set('_g', existingTab.globalState);
|
||||
await urlStateStorage.set('_a', existingTab.appState);
|
||||
} else {
|
||||
await urlStateStorage.set('_g', {});
|
||||
await urlStateStorage.set('_a', {});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setTabs({
|
||||
allTabs: updatedTabs,
|
||||
selectedTabId: selectedItem?.id ?? currentTab.id,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -17,8 +17,9 @@ import {
|
|||
} from 'react-redux';
|
||||
import React, { type PropsWithChildren, useMemo, createContext } from 'react';
|
||||
import { useAdHocDataViews } from './runtime_state';
|
||||
import type { DiscoverInternalState } from './types';
|
||||
import type { InternalStateDispatch, InternalStateStore } from './internal_state';
|
||||
import type { DiscoverInternalState, TabState } from './types';
|
||||
import { type InternalStateDispatch, type InternalStateStore } from './internal_state';
|
||||
import { selectCurrentTab } from './selectors';
|
||||
|
||||
const internalStateContext = createContext<ReactReduxContextValue>(
|
||||
// Recommended approach for versions of Redux prior to v9:
|
||||
|
@ -41,6 +42,9 @@ export const useInternalStateDispatch: () => InternalStateDispatch =
|
|||
export const useInternalStateSelector: TypedUseSelectorHook<DiscoverInternalState> =
|
||||
createSelectorHook(internalStateContext);
|
||||
|
||||
export const useCurrentTabSelector: TypedUseSelectorHook<TabState> = (selector) =>
|
||||
selector(useInternalStateSelector(selectCurrentTab));
|
||||
|
||||
export const useDataViewsForPicker = () => {
|
||||
const originalAdHocDataViews = useAdHocDataViews();
|
||||
const savedDataViews = useInternalStateSelector((state) => state.savedDataViews);
|
||||
|
|
|
@ -8,23 +8,33 @@
|
|||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { internalStateSlice, loadDataViewList } from './internal_state';
|
||||
import { internalStateSlice } from './internal_state';
|
||||
import {
|
||||
loadDataViewList,
|
||||
appendAdHocDataViews,
|
||||
initializeSession,
|
||||
replaceAdHocDataViewWithId,
|
||||
setAdHocDataViews,
|
||||
setDataView,
|
||||
setDefaultProfileAdHocDataViews,
|
||||
setTabs,
|
||||
updateTabs,
|
||||
} from './actions';
|
||||
|
||||
export type { DiscoverInternalState, InternalStateDataRequestParams } from './types';
|
||||
export type { DiscoverInternalState, TabState, InternalStateDataRequestParams } from './types';
|
||||
|
||||
export { type InternalStateStore, createInternalStateStore } from './internal_state';
|
||||
export { type InternalStateStore, createInternalStateStore, createTabItem } from './internal_state';
|
||||
|
||||
export const internalStateActions = {
|
||||
...omit(internalStateSlice.actions, 'setDataViewId', 'setDefaultProfileAdHocDataViewIds'),
|
||||
...omit(
|
||||
internalStateSlice.actions,
|
||||
'setTabs',
|
||||
'setDataViewId',
|
||||
'setDefaultProfileAdHocDataViewIds'
|
||||
),
|
||||
loadDataViewList,
|
||||
setTabs,
|
||||
updateTabs,
|
||||
setDataView,
|
||||
setAdHocDataViews,
|
||||
setDefaultProfileAdHocDataViews,
|
||||
|
@ -40,10 +50,14 @@ export {
|
|||
useDataViewsForPicker,
|
||||
} from './hooks';
|
||||
|
||||
export { selectAllTabs, selectCurrentTab } from './selectors';
|
||||
|
||||
export {
|
||||
type RuntimeStateManager,
|
||||
createRuntimeStateManager,
|
||||
useRuntimeState,
|
||||
selectCurrentTabRuntimeState,
|
||||
useCurrentTabRuntimeState,
|
||||
RuntimeStateProvider,
|
||||
useCurrentDataView,
|
||||
useAdHocDataViews,
|
||||
|
|
|
@ -8,7 +8,13 @@
|
|||
*/
|
||||
|
||||
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
|
||||
import { createInternalStateStore, createRuntimeStateManager, internalStateActions } from '.';
|
||||
import {
|
||||
createInternalStateStore,
|
||||
createRuntimeStateManager,
|
||||
internalStateActions,
|
||||
selectCurrentTab,
|
||||
selectCurrentTabRuntimeState,
|
||||
} from '.';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { mockCustomizationContext } from '../../../../customizations/__mocks__/customization_context';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -22,10 +28,14 @@ describe('InternalStateStore', () => {
|
|||
runtimeStateManager,
|
||||
urlStateStorage: createKbnUrlStateStorage(),
|
||||
});
|
||||
expect(store.getState().dataViewId).toBeUndefined();
|
||||
expect(runtimeStateManager.currentDataView$.value).toBeUndefined();
|
||||
expect(selectCurrentTab(store.getState()).dataViewId).toBeUndefined();
|
||||
expect(
|
||||
selectCurrentTabRuntimeState(store.getState(), runtimeStateManager).currentDataView$.value
|
||||
).toBeUndefined();
|
||||
store.dispatch(internalStateActions.setDataView(dataViewMock));
|
||||
expect(store.getState().dataViewId).toBe(dataViewMock.id);
|
||||
expect(runtimeStateManager.currentDataView$.value).toBe(dataViewMock);
|
||||
expect(selectCurrentTab(store.getState()).dataViewId).toBe(dataViewMock.id);
|
||||
expect(
|
||||
selectCurrentTabRuntimeState(store.getState(), runtimeStateManager).currentDataView$.value
|
||||
).toBe(dataViewMock);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,28 +15,32 @@ import {
|
|||
createSlice,
|
||||
type ThunkAction,
|
||||
type ThunkDispatch,
|
||||
createAsyncThunk,
|
||||
} from '@reduxjs/toolkit';
|
||||
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { TabItem } from '@kbn/unified-tabs';
|
||||
import type { DiscoverCustomizationContext } from '../../../../customizations';
|
||||
import type { DiscoverServices } from '../../../../build_services';
|
||||
import type { RuntimeStateManager } from './runtime_state';
|
||||
import { type RuntimeStateManager } from './runtime_state';
|
||||
import {
|
||||
LoadingStatus,
|
||||
type DiscoverInternalState,
|
||||
type InternalStateDataRequestParams,
|
||||
type TabState,
|
||||
} from './types';
|
||||
import { loadDataViewList, setTabs } from './actions';
|
||||
import { selectAllTabs, selectCurrentTab } from './selectors';
|
||||
|
||||
const initialState: DiscoverInternalState = {
|
||||
initializationState: { hasESData: false, hasUserDataView: false },
|
||||
const DEFAULT_TAB_LABEL = i18n.translate('discover.defaultTabLabel', {
|
||||
defaultMessage: 'Untitled session',
|
||||
});
|
||||
const DEFAULT_TAB_REGEX = new RegExp(`^${DEFAULT_TAB_LABEL}( \\d+)?$`);
|
||||
|
||||
export const defaultTabState: Omit<TabState, keyof TabItem> = {
|
||||
dataViewId: undefined,
|
||||
isDataViewLoading: false,
|
||||
defaultProfileAdHocDataViewIds: [],
|
||||
savedDataViews: [],
|
||||
expandedDoc: undefined,
|
||||
dataRequestParams: {},
|
||||
overriddenVisContextAfterInvalidation: undefined,
|
||||
isESQLToDataViewTransitionModalVisible: false,
|
||||
resetDefaultProfileState: {
|
||||
resetId: '',
|
||||
columns: false,
|
||||
|
@ -57,16 +61,31 @@ const initialState: DiscoverInternalState = {
|
|||
},
|
||||
};
|
||||
|
||||
const createInternalStateAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: DiscoverInternalState;
|
||||
dispatch: InternalStateDispatch;
|
||||
extra: InternalStateThunkDependencies;
|
||||
}>();
|
||||
const initialState: DiscoverInternalState = {
|
||||
initializationState: { hasESData: false, hasUserDataView: false },
|
||||
defaultProfileAdHocDataViewIds: [],
|
||||
savedDataViews: [],
|
||||
expandedDoc: undefined,
|
||||
isESQLToDataViewTransitionModalVisible: false,
|
||||
tabs: { byId: {}, allIds: [], currentId: '' },
|
||||
};
|
||||
|
||||
export const loadDataViewList = createInternalStateAsyncThunk(
|
||||
'internalState/loadDataViewList',
|
||||
async (_, { extra: { services } }) => services.dataViews.getIdsWithTitle(true)
|
||||
);
|
||||
export const createTabItem = (allTabs: TabState[]): TabItem => {
|
||||
const id = uuidv4();
|
||||
const untitledTabCount = allTabs.filter((tab) => DEFAULT_TAB_REGEX.test(tab.label.trim())).length;
|
||||
const label =
|
||||
untitledTabCount > 0 ? `${DEFAULT_TAB_LABEL} ${untitledTabCount}` : DEFAULT_TAB_LABEL;
|
||||
|
||||
return { id, label };
|
||||
};
|
||||
|
||||
const withCurrentTab = (state: DiscoverInternalState, fn: (tab: TabState) => void) => {
|
||||
const currentTab = selectCurrentTab(state);
|
||||
|
||||
if (currentTab) {
|
||||
fn(currentTab);
|
||||
}
|
||||
};
|
||||
|
||||
export const internalStateSlice = createSlice({
|
||||
name: 'internalState',
|
||||
|
@ -79,17 +98,31 @@ export const internalStateSlice = createSlice({
|
|||
state.initializationState = action.payload;
|
||||
},
|
||||
|
||||
setDataViewId: (state, action: PayloadAction<string | undefined>) => {
|
||||
if (action.payload !== state.dataViewId) {
|
||||
state.expandedDoc = undefined;
|
||||
}
|
||||
|
||||
state.dataViewId = action.payload;
|
||||
setTabs: (state, action: PayloadAction<{ allTabs: TabState[]; selectedTabId: string }>) => {
|
||||
state.tabs.byId = action.payload.allTabs.reduce<Record<string, TabState>>(
|
||||
(acc, tab) => ({
|
||||
...acc,
|
||||
[tab.id]: tab,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
state.tabs.allIds = action.payload.allTabs.map((tab) => tab.id);
|
||||
state.tabs.currentId = action.payload.selectedTabId;
|
||||
},
|
||||
|
||||
setIsDataViewLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isDataViewLoading = action.payload;
|
||||
},
|
||||
setDataViewId: (state, action: PayloadAction<string | undefined>) =>
|
||||
withCurrentTab(state, (tab) => {
|
||||
if (action.payload !== tab.dataViewId) {
|
||||
state.expandedDoc = undefined;
|
||||
}
|
||||
|
||||
tab.dataViewId = action.payload;
|
||||
}),
|
||||
|
||||
setIsDataViewLoading: (state, action: PayloadAction<boolean>) =>
|
||||
withCurrentTab(state, (tab) => {
|
||||
tab.isDataViewLoading = action.payload;
|
||||
}),
|
||||
|
||||
setDefaultProfileAdHocDataViewIds: (state, action: PayloadAction<string[]>) => {
|
||||
state.defaultProfileAdHocDataViewIds = action.payload;
|
||||
|
@ -99,16 +132,18 @@ export const internalStateSlice = createSlice({
|
|||
state.expandedDoc = action.payload;
|
||||
},
|
||||
|
||||
setDataRequestParams: (state, action: PayloadAction<InternalStateDataRequestParams>) => {
|
||||
state.dataRequestParams = action.payload;
|
||||
},
|
||||
setDataRequestParams: (state, action: PayloadAction<InternalStateDataRequestParams>) =>
|
||||
withCurrentTab(state, (tab) => {
|
||||
tab.dataRequestParams = action.payload;
|
||||
}),
|
||||
|
||||
setOverriddenVisContextAfterInvalidation: (
|
||||
state,
|
||||
action: PayloadAction<DiscoverInternalState['overriddenVisContextAfterInvalidation']>
|
||||
) => {
|
||||
state.overriddenVisContextAfterInvalidation = action.payload;
|
||||
},
|
||||
action: PayloadAction<TabState['overriddenVisContextAfterInvalidation']>
|
||||
) =>
|
||||
withCurrentTab(state, (tab) => {
|
||||
tab.overriddenVisContextAfterInvalidation = action.payload;
|
||||
}),
|
||||
|
||||
setIsESQLToDataViewTransitionModalVisible: (state, action: PayloadAction<boolean>) => {
|
||||
state.isESQLToDataViewTransitionModalVisible = action.payload;
|
||||
|
@ -116,23 +151,24 @@ export const internalStateSlice = createSlice({
|
|||
|
||||
setResetDefaultProfileState: {
|
||||
prepare: (
|
||||
resetDefaultProfileState: Omit<DiscoverInternalState['resetDefaultProfileState'], 'resetId'>
|
||||
resetDefaultProfileState: Omit<TabState['resetDefaultProfileState'], 'resetId'>
|
||||
) => ({
|
||||
payload: {
|
||||
...resetDefaultProfileState,
|
||||
resetId: uuidv4(),
|
||||
},
|
||||
}),
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<DiscoverInternalState['resetDefaultProfileState']>
|
||||
) => {
|
||||
state.resetDefaultProfileState = action.payload;
|
||||
},
|
||||
reducer: (state, action: PayloadAction<TabState['resetDefaultProfileState']>) =>
|
||||
withCurrentTab(state, (tab) => {
|
||||
tab.resetDefaultProfileState = action.payload;
|
||||
}),
|
||||
},
|
||||
|
||||
resetOnSavedSearchChange: (state) => {
|
||||
state.overriddenVisContextAfterInvalidation = undefined;
|
||||
withCurrentTab(state, (tab) => {
|
||||
tab.overriddenVisContextAfterInvalidation = undefined;
|
||||
});
|
||||
|
||||
state.expandedDoc = undefined;
|
||||
},
|
||||
},
|
||||
|
@ -150,13 +186,23 @@ export interface InternalStateThunkDependencies {
|
|||
urlStateStorage: IKbnUrlStateStorage;
|
||||
}
|
||||
|
||||
export const createInternalStateStore = (options: InternalStateThunkDependencies) =>
|
||||
configureStore({
|
||||
export const createInternalStateStore = (options: InternalStateThunkDependencies) => {
|
||||
const store = configureStore({
|
||||
reducer: internalStateSlice.reducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({ thunk: { extraArgument: options } }),
|
||||
});
|
||||
|
||||
// TEMPORARY: Create initial default tab
|
||||
const defaultTab: TabState = {
|
||||
...defaultTabState,
|
||||
...createTabItem(selectAllTabs(store.getState())),
|
||||
};
|
||||
store.dispatch(setTabs({ allTabs: [defaultTab], selectedTabId: defaultTab.id }));
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
export type InternalStateStore = ReturnType<typeof createInternalStateStore>;
|
||||
|
||||
export type InternalStateDispatch = InternalStateStore['dispatch'];
|
||||
|
|
|
@ -8,41 +8,72 @@
|
|||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import React, { type PropsWithChildren, createContext, useContext, useMemo, useState } from 'react';
|
||||
import React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject, skip } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { useInternalStateSelector } from './hooks';
|
||||
import type { DiscoverInternalState } from './types';
|
||||
|
||||
interface DiscoverRuntimeState {
|
||||
currentDataView: DataView;
|
||||
adHocDataViews: DataView[];
|
||||
}
|
||||
|
||||
type RuntimeStateManagerInternal<TNullable extends keyof DiscoverRuntimeState> = {
|
||||
[key in keyof DiscoverRuntimeState as `${key}$`]: BehaviorSubject<
|
||||
key extends TNullable ? DiscoverRuntimeState[key] | undefined : DiscoverRuntimeState[key]
|
||||
interface TabRuntimeState {
|
||||
currentDataView: DataView;
|
||||
}
|
||||
|
||||
type ReactiveRuntimeState<TState, TNullable extends keyof TState = never> = {
|
||||
[key in keyof TState & string as `${key}$`]: BehaviorSubject<
|
||||
key extends TNullable ? TState[key] | undefined : TState[key]
|
||||
>;
|
||||
};
|
||||
|
||||
export type RuntimeStateManager = RuntimeStateManagerInternal<'currentDataView'>;
|
||||
type ReactiveTabRuntimeState = ReactiveRuntimeState<TabRuntimeState, 'currentDataView'>;
|
||||
|
||||
export const createRuntimeStateManager = (): RuntimeStateManager => ({
|
||||
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
|
||||
adHocDataViews$: new BehaviorSubject<DataView[]>([]),
|
||||
});
|
||||
|
||||
export const useRuntimeState = <T,>(stateSubject$: BehaviorSubject<T>) => {
|
||||
const [stateObservable$] = useState(() => stateSubject$.pipe(skip(1)));
|
||||
return useObservable(stateObservable$, stateSubject$.getValue());
|
||||
export type RuntimeStateManager = ReactiveRuntimeState<DiscoverRuntimeState> & {
|
||||
tabs: { byId: Record<string, ReactiveTabRuntimeState> };
|
||||
};
|
||||
|
||||
const runtimeStateContext = createContext<DiscoverRuntimeState | undefined>(undefined);
|
||||
export const createRuntimeStateManager = (): RuntimeStateManager => ({
|
||||
adHocDataViews$: new BehaviorSubject<DataView[]>([]),
|
||||
tabs: { byId: {} },
|
||||
});
|
||||
|
||||
export const createTabRuntimeState = (): ReactiveTabRuntimeState => ({
|
||||
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
|
||||
});
|
||||
|
||||
export const useRuntimeState = <T,>(stateSubject$: BehaviorSubject<T>) =>
|
||||
useObservable(stateSubject$, stateSubject$.getValue());
|
||||
|
||||
export const selectCurrentTabRuntimeState = (
|
||||
internalState: DiscoverInternalState,
|
||||
runtimeStateManager: RuntimeStateManager
|
||||
) => {
|
||||
const currentTabId = internalState.tabs.currentId;
|
||||
return runtimeStateManager.tabs.byId[currentTabId];
|
||||
};
|
||||
|
||||
export const useCurrentTabRuntimeState = <T,>(
|
||||
runtimeStateManager: RuntimeStateManager,
|
||||
selector: (tab: ReactiveTabRuntimeState) => BehaviorSubject<T>
|
||||
) => {
|
||||
const tab = useInternalStateSelector((state) =>
|
||||
selectCurrentTabRuntimeState(state, runtimeStateManager)
|
||||
);
|
||||
return useRuntimeState(selector(tab));
|
||||
};
|
||||
|
||||
type CombinedRuntimeState = DiscoverRuntimeState & TabRuntimeState;
|
||||
|
||||
const runtimeStateContext = createContext<CombinedRuntimeState | undefined>(undefined);
|
||||
|
||||
export const RuntimeStateProvider = ({
|
||||
currentDataView,
|
||||
adHocDataViews,
|
||||
children,
|
||||
}: PropsWithChildren<DiscoverRuntimeState>) => {
|
||||
const runtimeState = useMemo<DiscoverRuntimeState>(
|
||||
}: PropsWithChildren<CombinedRuntimeState>) => {
|
||||
const runtimeState = useMemo<CombinedRuntimeState>(
|
||||
() => ({ currentDataView, adHocDataViews }),
|
||||
[adHocDataViews, currentDataView]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 type { DiscoverInternalState } from './types';
|
||||
|
||||
export const selectAllTabs = (state: DiscoverInternalState) =>
|
||||
state.tabs.allIds.map((id) => state.tabs.byId[id]);
|
||||
|
||||
export const selectCurrentTab = (state: DiscoverInternalState) =>
|
||||
state.tabs.byId[state.tabs.currentId];
|
|
@ -11,6 +11,7 @@ import type { DataViewListItem } from '@kbn/data-views-plugin/public';
|
|||
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
|
||||
import type { TabItem } from '@kbn/unified-tabs';
|
||||
|
||||
export enum LoadingStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
|
@ -42,16 +43,13 @@ export interface InternalStateDataRequestParams {
|
|||
timeRangeRelative?: TimeRange;
|
||||
}
|
||||
|
||||
export interface DiscoverInternalState {
|
||||
initializationState: { hasESData: boolean; hasUserDataView: boolean };
|
||||
export interface TabState extends TabItem {
|
||||
globalState?: Record<string, unknown>;
|
||||
appState?: Record<string, unknown>;
|
||||
dataViewId: string | undefined;
|
||||
isDataViewLoading: boolean;
|
||||
savedDataViews: DataViewListItem[];
|
||||
defaultProfileAdHocDataViewIds: string[];
|
||||
expandedDoc: DataTableRecord | undefined;
|
||||
dataRequestParams: InternalStateDataRequestParams;
|
||||
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving
|
||||
isESQLToDataViewTransitionModalVisible: boolean;
|
||||
resetDefaultProfileState: {
|
||||
resetId: string;
|
||||
columns: boolean;
|
||||
|
@ -62,3 +60,16 @@ export interface DiscoverInternalState {
|
|||
totalHitsRequest: TotalHitsRequest;
|
||||
chartRequest: ChartRequest;
|
||||
}
|
||||
|
||||
export interface DiscoverInternalState {
|
||||
initializationState: { hasESData: boolean; hasUserDataView: boolean };
|
||||
savedDataViews: DataViewListItem[];
|
||||
defaultProfileAdHocDataViewIds: string[];
|
||||
expandedDoc: DataTableRecord | undefined;
|
||||
isESQLToDataViewTransitionModalVisible: boolean;
|
||||
tabs: {
|
||||
byId: Record<string, TabState>;
|
||||
allIds: string[];
|
||||
currentId: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import type { DiscoverInternalState } from './types';
|
||||
import type { InternalStateDispatch, InternalStateThunkDependencies } from './internal_state';
|
||||
|
||||
// For some reason if this is not explicitly typed, TypeScript fails with the following error:
|
||||
// TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
|
||||
type CreateInternalStateAsyncThunk = ReturnType<
|
||||
typeof createAsyncThunk.withTypes<{
|
||||
state: DiscoverInternalState;
|
||||
dispatch: InternalStateDispatch;
|
||||
extra: InternalStateThunkDependencies;
|
||||
}>
|
||||
>;
|
||||
|
||||
export const createInternalStateAsyncThunk: CreateInternalStateAsyncThunk =
|
||||
createAsyncThunk.withTypes();
|
|
@ -17,7 +17,7 @@ import { discoverServiceMock } from '../../../../__mocks__/services';
|
|||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { createDataViewDataSource } from '../../../../../common/data_sources';
|
||||
import { createRuntimeStateManager, internalStateActions } from '../redux';
|
||||
import { createRuntimeStateManager, internalStateActions, selectCurrentTab } from '../redux';
|
||||
|
||||
const setupTestParams = (dataView: DataView | undefined) => {
|
||||
const savedSearch = savedSearchMock;
|
||||
|
@ -41,41 +41,41 @@ describe('changeDataView', () => {
|
|||
it('should set the right app state when a valid data view (which includes the preconfigured default column) to switch to is given', async () => {
|
||||
const params = setupTestParams(dataViewWithDefaultColumnMock);
|
||||
const promise = changeDataView({ dataViewId: dataViewWithDefaultColumnMock.id!, ...params });
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(true);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
|
||||
await promise;
|
||||
expect(params.appState.update).toHaveBeenCalledWith({
|
||||
columns: ['default_column'], // default_column would be added as dataViewWithDefaultColumn has it as a mapped field
|
||||
dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-user-default-column-id' }),
|
||||
sort: [['@timestamp', 'desc']],
|
||||
});
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(false);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the right app state when a valid data view to switch to is given', async () => {
|
||||
const params = setupTestParams(dataViewComplexMock);
|
||||
const promise = changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(true);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
|
||||
await promise;
|
||||
expect(params.appState.update).toHaveBeenCalledWith({
|
||||
columns: [], // default_column would not be added as dataViewComplexMock does not have it as a mapped field
|
||||
dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-various-field-types-id' }),
|
||||
sort: [['data', 'desc']],
|
||||
});
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(false);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should not set the app state when an invalid data view to switch to is given', async () => {
|
||||
const params = setupTestParams(undefined);
|
||||
const promise = changeDataView({ dataViewId: 'data-view-with-various-field-types', ...params });
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(true);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
|
||||
await promise;
|
||||
expect(params.appState.update).not.toHaveBeenCalled();
|
||||
expect(params.internalState.getState().isDataViewLoading).toBe(false);
|
||||
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should call setResetDefaultProfileState correctly when switching data view', async () => {
|
||||
const params = setupTestParams(dataViewComplexMock);
|
||||
expect(params.internalState.getState().resetDefaultProfileState).toEqual(
|
||||
expect(selectCurrentTab(params.internalState.getState()).resetDefaultProfileState).toEqual(
|
||||
expect.objectContaining({
|
||||
columns: false,
|
||||
rowHeight: false,
|
||||
|
@ -83,7 +83,7 @@ describe('changeDataView', () => {
|
|||
})
|
||||
);
|
||||
await changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
|
||||
expect(params.internalState.getState().resetDefaultProfileState).toEqual(
|
||||
expect(selectCurrentTab(params.internalState.getState()).resetDefaultProfileState).toEqual(
|
||||
expect.objectContaining({
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
|
|
|
@ -18,7 +18,12 @@ import type { DiscoverAppStateContainer } from '../discover_app_state_container'
|
|||
import { addLog } from '../../../../utils/add_log';
|
||||
import type { DiscoverServices } from '../../../../build_services';
|
||||
import { getDataViewAppState } from './get_switch_data_view_app_state';
|
||||
import { internalStateActions, type InternalStateStore, type RuntimeStateManager } from '../redux';
|
||||
import {
|
||||
internalStateActions,
|
||||
selectCurrentTabRuntimeState,
|
||||
type InternalStateStore,
|
||||
type RuntimeStateManager,
|
||||
} from '../redux';
|
||||
|
||||
/**
|
||||
* Function executed when switching data view in the UI
|
||||
|
@ -39,7 +44,11 @@ export async function changeDataView({
|
|||
addLog('[ui] changeDataView', { id: dataViewId });
|
||||
|
||||
const { dataViews, uiSettings } = services;
|
||||
const currentDataView = runtimeStateManager.currentDataView$.getValue();
|
||||
const { currentDataView$ } = selectCurrentTabRuntimeState(
|
||||
internalState.getState(),
|
||||
runtimeStateManager
|
||||
);
|
||||
const currentDataView = currentDataView$.getValue();
|
||||
const state = appState.getState();
|
||||
let nextDataView: DataView | null = null;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { DiscoverAppState } from '../discover_app_state_container';
|
|||
import type { DefaultAppStateColumn, ProfilesManager } from '../../../../context_awareness';
|
||||
import { getMergedAccessor } from '../../../../context_awareness';
|
||||
import type { DataDocumentsMsg } from '../discover_data_state_container';
|
||||
import type { DiscoverInternalState } from '../redux';
|
||||
import type { TabState } from '../redux';
|
||||
|
||||
export const getDefaultProfileState = ({
|
||||
profilesManager,
|
||||
|
@ -22,7 +22,7 @@ export const getDefaultProfileState = ({
|
|||
dataView,
|
||||
}: {
|
||||
profilesManager: ProfilesManager;
|
||||
resetDefaultProfileState: DiscoverInternalState['resetDefaultProfileState'];
|
||||
resetDefaultProfileState: TabState['resetDefaultProfileState'];
|
||||
dataView: DataView;
|
||||
}) => {
|
||||
const defaultState = getDefaultState(profilesManager, dataView);
|
||||
|
|
|
@ -103,7 +103,8 @@
|
|||
"@kbn/response-ops-rule-form",
|
||||
"@kbn/embeddable-enhanced-plugin",
|
||||
"@kbn/shared-ux-page-analytics-no-data-types",
|
||||
"@kbn/core-application-browser-mocks"
|
||||
"@kbn/core-application-browser-mocks",
|
||||
"@kbn/unified-tabs"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ jest.mock('uuid', () => ({
|
|||
.fn()
|
||||
.mockReturnValueOnce('d594baeb-5eca-480c-8885-ba79eaf41372')
|
||||
.mockReturnValueOnce('c604baeb-5eca-480c-8885-ba79eaf41372')
|
||||
// This ID is used by the Discover state container created in MockDiscoverInTimelineContext
|
||||
.mockReturnValueOnce('mock-id-for-discover')
|
||||
.mockReturnValueOnce('e614baeb-5eca-480c-8885-ba79eaf41372')
|
||||
.mockReturnValueOnce('f614baeb-5eca-480c-8885-ba79eaf52483')
|
||||
.mockReturnValue('1dd5663b-f062-43f8-8688-fc8166c2ca8e'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue