[Discover] Support state updates across tabs (#215620)

## Summary

This PR adjusts the approach introduced in #214861 to ensure state
updates work consistently across tabs, even after switching tabs during
async operations. The `currentTabId` prop has been removed from the
central state since it can't be relied on in actions, and instead tab
IDs are injected using a `CurrentTabProvider`. This allows selectors to
work the same as they did before, and tab specific actions have been
updated to use a standard `TabAction` interface that accepts a tab ID
and prevents leaking state changes.

This approach is safer but adds some complexity, so for actions
dispatched from React components, a `useCurrentTabAction` hook has been
added to handle injecting the current tab ID. We also still need to
access tab state within `DiscoverStateContainer` for now, so two utility
methods (`injectCurrentTab` and `getCurrentTab`) have been added to make
this easier. Since `DiscoverStateContainer` is scoped to a single tab,
this should be safe, and ideally temporary until we get rid of it
completely.

Resolves #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
- [x] [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)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2025-03-31 15:41:59 -03:00 committed by GitHub
parent 63575a8320
commit bf7de0e6b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 813 additions and 593 deletions

View file

@ -240,7 +240,7 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
ControlGroupRendererApi | undefined
>();
const stateStorage = stateContainer.stateStorage;
const currentTabId = stateContainer.internalState.getState().tabs.currentId;
const currentTabId = stateContainer.getCurrentTab().id;
const dataView = useObservable(
stateContainer.runtimeStateManager.tabs.byId[currentTabId].currentDataView$,
stateContainer.runtimeStateManager.tabs.byId[currentTabId].currentDataView$.getValue()

View file

@ -55,16 +55,18 @@ export function getDiscoverStateMock({
...(toasts && withNotifyOnErrors(toasts)),
});
runtimeStateManager = runtimeStateManager ?? createRuntimeStateManager();
const internalState = createInternalStateStore({
services,
customizationContext,
runtimeStateManager,
urlStateStorage: stateStorageContainer,
});
const container = getDiscoverStateContainer({
tabId: internalState.getState().tabs.allIds[0],
services,
customizationContext,
stateStorageContainer,
internalState: createInternalStateStore({
services,
customizationContext,
runtimeStateManager,
urlStateStorage: stateStorageContainer,
}),
internalState,
runtimeStateManager,
});
if (savedSearch !== false) {

View file

@ -30,7 +30,7 @@ import { createCustomizationService } from '../../../../customizations/customiza
import { DiscoverGrid } from '../../../../components/discover_grid';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import type { ProfilesManager } from '../../../../context_awareness';
import { internalStateActions } from '../../state_management/redux';
import { CurrentTabProvider, internalStateActions } from '../../state_management/redux';
const customisationService = createCustomizationService();
@ -54,14 +54,16 @@ async function mountComponent(
dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
});
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
stateContainer.injectCurrentTab(internalStateActions.setDataRequestParams)({
dataRequestParams: {
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
},
})
);
@ -81,11 +83,13 @@ async function mountComponent(
services={{ ...services, profilesManager: profilesManager ?? services.profilesManager }}
>
<DiscoverCustomizationProvider value={customisationService}>
<DiscoverMainProvider value={stateContainer}>
<EuiProvider highContrastMode={false}>
<DiscoverDocuments {...props} />
</EuiProvider>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<EuiProvider highContrastMode={false}>
<DiscoverDocuments {...props} />
</EuiProvider>
</DiscoverMainProvider>
</CurrentTabProvider>
</DiscoverCustomizationProvider>
</KibanaContextProvider>
);

View file

@ -76,10 +76,10 @@ import {
} from '../../../../context_awareness';
import {
internalStateActions,
useCurrentTabSelector,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
const DiscoverGridMemoized = React.memo(DiscoverGrid);

View file

@ -37,7 +37,11 @@ import { DiscoverMainProvider } from '../../state_management/discover_state_prov
import { act } from 'react-dom/test-utils';
import { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
import {
CurrentTabProvider,
RuntimeStateProvider,
internalStateActions,
} from '../../state_management/redux';
function getStateContainer(savedSearch?: SavedSearch) {
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
@ -50,16 +54,20 @@ function getStateContainer(savedSearch?: SavedSearch) {
stateContainer.appState.update(appState);
stateContainer.internalState.dispatch(internalStateActions.setDataView(dataView));
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
stateContainer.injectCurrentTab(internalStateActions.setDataView)({ dataView })
);
stateContainer.internalState.dispatch(
stateContainer.injectCurrentTab(internalStateActions.setDataRequestParams)({
dataRequestParams: {
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
},
})
);
@ -147,11 +155,13 @@ const mountComponent = async ({
const component = mountWithIntl(
<KibanaRenderContextProvider {...services.core}>
<KibanaContextProvider services={services}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<DiscoverHistogramLayout {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<DiscoverHistogramLayout {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
</CurrentTabProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>
);

View file

@ -40,7 +40,11 @@ import { act } from 'react-dom/test-utils';
import { ErrorCallout } from '../../../../components/common/error_callout';
import { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
import {
CurrentTabProvider,
RuntimeStateProvider,
internalStateActions,
} from '../../state_management/redux';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
@ -105,9 +109,13 @@ async function mountComponent(
interval: 'auto',
query,
});
stateContainer.internalState.dispatch(internalStateActions.setDataView(dataView));
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({ timeRangeAbsolute: time, timeRangeRelative: time })
stateContainer.injectCurrentTab(internalStateActions.setDataView)({ dataView })
);
stateContainer.internalState.dispatch(
stateContainer.injectCurrentTab(internalStateActions.setDataRequestParams)({
dataRequestParams: { timeRangeAbsolute: time, timeRangeRelative: time },
})
);
const props = {
@ -127,13 +135,15 @@ async function mountComponent(
const component = mountWithIntl(
<KibanaContextProvider services={services}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<EuiProvider highContrastMode={false}>
<DiscoverLayout {...props} />
</EuiProvider>
</RuntimeStateProvider>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<EuiProvider highContrastMode={false}>
<DiscoverLayout {...props} />
</EuiProvider>
</RuntimeStateProvider>
</DiscoverMainProvider>
</CurrentTabProvider>
</KibanaContextProvider>,
mountOptions
);

View file

@ -57,8 +57,7 @@ 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 } from '../../state_management/redux';
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav);

View file

@ -39,6 +39,7 @@ import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'
import { PanelsToggle } from '../../../../components/panels_toggle';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import { CurrentTabProvider } from '../../state_management/redux';
const mountComponent = async ({
hideChart = false,
@ -130,9 +131,11 @@ const mountComponent = async ({
const component = mountWithIntl(
<KibanaRenderContextProvider {...services.core}>
<KibanaContextProvider services={services}>
<DiscoverMainProvider value={stateContainer}>
<DiscoverMainContent {...props} />
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<DiscoverMainContent {...props} />
</DiscoverMainProvider>
</CurrentTabProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>
);

View file

@ -29,7 +29,11 @@ import type { InspectorAdapters } from '../../hooks/use_inspector';
import type { UnifiedHistogramCustomization } from '../../../../customizations/customization_types/histogram_customization';
import { useDiscoverCustomization } from '../../../../customizations';
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
import {
CurrentTabProvider,
RuntimeStateProvider,
internalStateActions,
} from '../../state_management/redux';
import { dataViewMockWithTimeField } from '@kbn/discover-utils/src/__mocks__';
const mockData = dataPluginMock.createStartContract();
@ -123,11 +127,13 @@ describe('useDiscoverHistogram', () => {
};
const Wrapper = ({ children }: React.PropsWithChildren<unknown>) => (
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMockWithTimeField} adHocDataViews={[]}>
{children as ReactElement}
</RuntimeStateProvider>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMockWithTimeField} adHocDataViews={[]}>
{children as ReactElement}
</RuntimeStateProvider>
</DiscoverMainProvider>
</CurrentTabProvider>
);
const hook = renderHook(
@ -391,9 +397,11 @@ describe('useDiscoverHistogram', () => {
const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
const timeRangeRel = { from: 'now-15m', to: 'now' };
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: timeRangeAbs,
timeRangeRelative: timeRangeRel,
stateContainer.injectCurrentTab(internalStateActions.setDataRequestParams)({
dataRequestParams: {
timeRangeAbsolute: timeRangeAbs,
timeRangeRelative: timeRangeRel,
},
})
);
const { hook } = await renderUseDiscoverHistogram({ stateContainer });

View file

@ -57,9 +57,10 @@ import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import {
internalStateActions,
useCurrentDataView,
useCurrentTabAction,
useCurrentTabSelector,
useInternalStateDispatch,
} from '../../state_management/redux';
import { useCurrentTabSelector } from '../../state_management/redux/hooks';
const EMPTY_ESQL_COLUMNS: DatatableColumn[] = [];
const EMPTY_FILTERS: Filter[] = [];
@ -321,6 +322,9 @@ export const useDiscoverHistogram = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
const timeRangeMemoized = useMemo(() => timeRange, [timeRange?.from, timeRange?.to]);
const setOverriddenVisContextAfterInvalidation = useCurrentTabAction(
internalStateActions.setOverriddenVisContextAfterInvalidation
);
const dispatch = useInternalStateDispatch();
const onVisContextChanged = useCallback(
@ -335,25 +339,41 @@ export const useDiscoverHistogram = ({
stateContainer.savedSearchState.updateVisContext({
nextVisContext,
});
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
dispatch(
setOverriddenVisContextAfterInvalidation({
overriddenVisContextAfterInvalidation: undefined,
})
);
break;
case UnifiedHistogramExternalVisContextStatus.automaticallyOverridden:
// if the visualization was invalidated as incompatible and rebuilt
// (it will be used later for saving the visualization via Save button)
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(nextVisContext));
dispatch(
setOverriddenVisContextAfterInvalidation({
overriddenVisContextAfterInvalidation: nextVisContext,
})
);
break;
case UnifiedHistogramExternalVisContextStatus.automaticallyCreated:
case UnifiedHistogramExternalVisContextStatus.applied:
// clearing the value in the internal state so we don't use it during saved search saving
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
dispatch(
setOverriddenVisContextAfterInvalidation({
overriddenVisContextAfterInvalidation: undefined,
})
);
break;
case UnifiedHistogramExternalVisContextStatus.unknown:
// using `{}` to overwrite the value inside the saved search SO during saving
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation({}));
dispatch(
setOverriddenVisContextAfterInvalidation({
overriddenVisContextAfterInvalidation: {},
})
);
break;
}
},
[dispatch, stateContainer.savedSearchState]
[dispatch, setOverriddenVisContextAfterInvalidation, stateContainer.savedSearchState]
);
const breakdownField = useAppStateSelector((state) => state.breakdownField);

View file

@ -20,7 +20,11 @@ import { Router } from '@kbn/shared-ux-router';
import { createMemoryHistory } from 'history';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
import {
CurrentTabProvider,
RuntimeStateProvider,
internalStateActions,
} from '../../state_management/redux';
discoverServiceMock.data.query.timefilter.timefilter.getTime = () => {
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
@ -46,11 +50,13 @@ describe('DiscoverMainApp', () => {
const component = mountWithIntl(
<Router history={history}>
<KibanaContextProvider services={discoverServiceMock}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverMainApp {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverMainApp {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
</CurrentTabProvider>
</KibanaContextProvider>
</Router>
);

View file

@ -29,6 +29,8 @@ import {
useInternalStateSelector,
useRuntimeState,
useCurrentTabRuntimeState,
useCurrentTabSelector,
useCurrentTabAction,
} from '../../state_management/redux';
import type {
CustomizationCallback,
@ -89,11 +91,14 @@ export const DiscoverSessionView = forwardRef<DiscoverSessionViewRef, DiscoverSe
const services = useDiscoverServices();
const { core, history, getScopedHistory } = services;
const { id: discoverSessionId } = useParams<{ id?: string }>();
const currentTabId = useCurrentTabSelector((tab) => tab.id);
const initializeSessionAction = useCurrentTabAction(internalStateActions.initializeSession);
const [initializeSessionState, initializeSession] = useAsyncFunction<InitializeSession>(
async ({ dataViewSpec, defaultUrlState } = {}) => {
initializeSessionState.value?.stateContainer?.actions.stopSyncing();
const stateContainer = getDiscoverStateContainer({
tabId: currentTabId,
services,
customizationContext,
stateStorageContainer: urlStateStorage,
@ -101,11 +106,13 @@ export const DiscoverSessionView = forwardRef<DiscoverSessionViewRef, DiscoverSe
runtimeStateManager,
});
const { showNoDataPage } = await dispatch(
internalStateActions.initializeSession({
stateContainer,
discoverSessionId,
dataViewSpec,
defaultUrlState,
initializeSessionAction({
initializeSessionParams: {
stateContainer,
discoverSessionId,
dataViewSpec,
defaultUrlState,
},
})
);

View file

@ -13,20 +13,26 @@ import { pick } from 'lodash';
import type { DiscoverSessionViewRef } from '../session_view';
import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view';
import {
CurrentTabProvider,
createTabItem,
internalStateActions,
selectAllTabs,
selectCurrentTab,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export const TabsView = ({ sessionViewProps }: { sessionViewProps: DiscoverSessionViewProps }) => {
export const TabsView = ({
initialTabId,
sessionViewProps,
}: {
initialTabId: string;
sessionViewProps: DiscoverSessionViewProps;
}) => {
const services = useDiscoverServices();
const dispatch = useInternalStateDispatch();
const currentTab = useInternalStateSelector(selectCurrentTab);
const allTabs = useInternalStateSelector(selectAllTabs);
const [currentTabId, setCurrentTabId] = useState(initialTabId);
const [initialItems] = useState<TabItem[]>(() => allTabs.map((tab) => pick(tab, 'id', 'label')));
const sessionViewRef = useRef<DiscoverSessionViewRef>(null);
@ -34,21 +40,25 @@ export const TabsView = ({ sessionViewProps }: { sessionViewProps: DiscoverSessi
<UnifiedTabs
services={services}
initialItems={initialItems}
onChanged={(updateState) =>
dispatch(
onChanged={async (updateState) => {
await dispatch(
internalStateActions.updateTabs({
currentTabId,
updateState,
stopSyncing: sessionViewRef.current?.stopSyncing,
})
)
}
);
setCurrentTabId(updateState.selectedItem?.id ?? currentTabId);
}}
createItem={() => createTabItem(allTabs)}
getPreviewData={() => ({
query: { language: 'kuery', query: 'sample query' },
status: TabStatus.SUCCESS,
})}
renderContent={() => (
<DiscoverSessionView key={currentTab.id} ref={sessionViewRef} {...sessionViewProps} />
<CurrentTabProvider currentTabId={currentTabId}>
<DiscoverSessionView key={currentTabId} ref={sessionViewRef} {...sessionViewProps} />
</CurrentTabProvider>
)}
/>
);

View file

@ -73,7 +73,9 @@ function getProps(
mockDiscoverService.capabilities = capabilities as typeof mockDiscoverService.capabilities;
}
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.dispatch(internalStateActions.setDataView(dataViewMock));
stateContainer.internalState.dispatch(
stateContainer.injectCurrentTab(internalStateActions.setDataView)({ dataView: dataViewMock })
);
return {
stateContainer,

View file

@ -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, selectCurrentTab } from '../../state_management/redux';
import { internalStateActions } from '../../state_management/redux';
async function saveDataSource({
savedSearch,
@ -95,7 +95,7 @@ export async function onSaveSearch({
}) {
const { uiSettings, savedObjectsTagging } = services;
const dataView = savedSearch.searchSource.getField('index');
const currentTab = selectCurrentTab(state.internalState.getState());
const currentTab = state.getCurrentTab();
const overriddenVisContextAfterInvalidation = currentTab.overriddenVisContextAfterInvalidation;
const onSave = async ({
@ -176,7 +176,9 @@ export async function onSaveSearch({
savedSearch.tags = currentTags;
}
} else {
state.internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
state.internalState.dispatch(
state.injectCurrentTab(internalStateActions.resetOnSavedSearchChange)()
);
state.appState.resetInitialState();
}

View file

@ -28,6 +28,7 @@ import { buildDataTableRecord } from '@kbn/discover-utils';
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
import type { TabState } from '../state_management/redux';
jest.mock('./fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue([]),
@ -57,6 +58,8 @@ describe('test fetchAll', () => {
let subjects: SavedSearchData;
let deps: Parameters<typeof fetchAll>[2];
let searchSource: SearchSource;
let getCurrentTab: () => TabState;
beforeEach(() => {
subjects = {
main$: new BehaviorSubject<DataMainMsg>({ fetchStatus: FetchStatus.UNINITIALIZED }),
@ -64,12 +67,12 @@ describe('test fetchAll', () => {
totalHits$: new BehaviorSubject<DataTotalHitsMsg>({ fetchStatus: FetchStatus.UNINITIALIZED }),
};
searchSource = savedSearchMock.searchSource.createChild();
const { internalState, getCurrentTab: localGetCurrentTab } = getDiscoverStateMock({});
deps = {
abortController: new AbortController(),
inspectorAdapters: { requests: new RequestAdapter() },
getAppState: () => ({}),
internalState: getDiscoverStateMock({}).internalState,
internalState,
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
savedSearch: {
@ -78,7 +81,7 @@ describe('test fetchAll', () => {
},
services: discoverServiceMock,
};
getCurrentTab = localGetCurrentTab;
mockFetchDocuments.mockReset().mockResolvedValue({ records: [] });
mockfetchEsql.mockReset().mockResolvedValue({ records: [] });
});
@ -88,7 +91,7 @@ describe('test fetchAll', () => {
subjects.main$.subscribe((value) => stateArr.push(value.fetchStatus));
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
expect(stateArr).toEqual([
@ -106,7 +109,7 @@ describe('test fetchAll', () => {
];
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
mockFetchDocuments.mockResolvedValue({ records: documents });
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
@ -131,7 +134,7 @@ describe('test fetchAll', () => {
subjects.totalHits$.next({
fetchStatus: FetchStatus.LOADING,
});
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
subjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
@ -152,7 +155,7 @@ describe('test fetchAll', () => {
subjects.totalHits$.next({
fetchStatus: FetchStatus.LOADING,
});
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
subjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
@ -177,7 +180,7 @@ describe('test fetchAll', () => {
subjects.totalHits$.next({
fetchStatus: FetchStatus.LOADING,
});
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
subjects.totalHits$.next({
fetchStatus: FetchStatus.ERROR,
@ -209,7 +212,7 @@ describe('test fetchAll', () => {
subjects.totalHits$.next({
fetchStatus: FetchStatus.LOADING,
});
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
subjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
@ -249,7 +252,7 @@ describe('test fetchAll', () => {
getAppState: () => ({ query }),
internalState: getDiscoverStateMock({}).internalState,
};
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
await waitForNextTick();
expect(await collect()).toEqual([
@ -358,7 +361,7 @@ describe('test fetchAll', () => {
getAppState: () => ({ query }),
internalState: getDiscoverStateMock({}).internalState,
};
fetchAll(subjects, false, deps);
fetchAll(subjects, false, deps, getCurrentTab);
deps.abortController.abort();
await waitForNextTick();

View file

@ -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 { selectCurrentTab, type InternalStateStore } from '../state_management/redux';
import { type InternalStateStore, type TabState } from '../state_management/redux';
export interface FetchDeps {
abortController: AbortController;
@ -59,12 +59,12 @@ export function fetchAll(
dataSubjects: SavedSearchData,
reset = false,
fetchDeps: FetchDeps,
getCurrentTab: () => TabState,
onFetchRecordsComplete?: () => Promise<void>
): Promise<void> {
const {
initialFetchStatus,
getAppState,
internalState,
services,
inspectorAdapters,
savedSearch,
@ -78,7 +78,7 @@ export function fetchAll(
const query = getAppState().query;
const prevQuery = dataSubjects.documents$.getValue().query;
const isEsqlQuery = isOfAggregateQueryType(query);
const currentTab = selectCurrentTab(internalState.getState());
const currentTab = getCurrentTab();
if (reset) {
sendResetMsg(dataSubjects, initialFetchStatus);

View file

@ -20,6 +20,7 @@ import {
createInternalStateStore,
createRuntimeStateManager,
internalStateActions,
CurrentTabProvider,
} from './state_management/redux';
import type { RootProfileState } from '../../context_awareness';
import { useRootProfile, useDefaultAdHocDataViews } from '../../context_awareness';
@ -75,6 +76,7 @@ export const DiscoverMainRoute = ({
urlStateStorage,
})
);
const [initialTabId] = useState(() => internalState.getState().tabs.allIds[0]);
const { initializeProfileDataViews } = useDefaultAdHocDataViews({ internalState });
const [mainRouteInitializationState, initializeMainRoute] = useAsyncFunction<InitializeMainRoute>(
async (loadedRootProfileState) => {
@ -137,9 +139,11 @@ export const DiscoverMainRoute = ({
<InternalStateProvider store={internalState}>
<rootProfileState.AppWrapper>
{TABS_ENABLED ? (
<TabsView sessionViewProps={sessionViewProps} />
<TabsView initialTabId={initialTabId} sessionViewProps={sessionViewProps} />
) : (
<DiscoverSessionView {...sessionViewProps} />
<CurrentTabProvider currentTabId={initialTabId}>
<DiscoverSessionView {...sessionViewProps} />
</CurrentTabProvider>
)}
</rootProfileState.AppWrapper>
</InternalStateProvider>

View file

@ -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, selectCurrentTab } from '../state_management/redux';
import { CurrentTabProvider, internalStateActions } from '../state_management/redux';
async function getHookProps(
query: AggregateQuery | Query | undefined,
@ -82,9 +82,11 @@ const getDataViewsService = () => {
const getHookContext = (stateContainer: DiscoverStateContainer) => {
return ({ children }: React.PropsWithChildren) => (
<DiscoverMainProvider value={stateContainer}>
<>{children}</>
</DiscoverMainProvider>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}>
<>{children}</>
</DiscoverMainProvider>
</CurrentTabProvider>
);
};
const renderHookWithContext = async (
@ -506,12 +508,7 @@ describe('useEsqlMode', () => {
FetchStatus.LOADING
);
const documents$ = stateContainer.dataState.data$.documents$;
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -526,12 +523,7 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -542,10 +534,12 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern1' },
});
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
stateContainer.injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: false,
rowHeight: false,
breakdownField: false,
},
})
);
stateContainer.appState.update({ query: { esql: 'from pattern1' } });
@ -554,12 +548,7 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -575,12 +564,7 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern2' },
});
await waitFor(() =>
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -597,12 +581,7 @@ describe('useEsqlMode', () => {
const documents$ = stateContainer.dataState.data$.documents$;
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -613,12 +592,7 @@ describe('useEsqlMode', () => {
result: result1,
});
await waitFor(() =>
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -630,12 +604,7 @@ describe('useEsqlMode', () => {
result: result2,
});
await waitFor(() =>
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: false,
breakdownField: false,

View file

@ -17,7 +17,11 @@ import { useSavedSearchInitial } from '../state_management/discover_state_provid
import type { DiscoverStateContainer } from '../state_management/discover_state';
import { getValidViewMode } from '../utils/get_valid_view_mode';
import { FetchStatus } from '../../types';
import { internalStateActions, useInternalStateDispatch } from '../state_management/redux';
import {
internalStateActions,
useCurrentTabAction,
useInternalStateDispatch,
} from '../state_management/redux';
const MAX_NUM_OF_COLUMNS = 50;
@ -32,6 +36,9 @@ export function useEsqlMode({
stateContainer: DiscoverStateContainer;
dataViews: DataViewsContract;
}) {
const setResetDefaultProfileState = useCurrentTabAction(
internalStateActions.setResetDefaultProfileState
);
const dispatch = useInternalStateDispatch();
const savedSearch = useSavedSearchInitial();
const prev = useRef<{
@ -96,10 +103,12 @@ export function useEsqlMode({
// Reset all default profile state when index pattern changes
if (indexPatternChanged) {
dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
setResetDefaultProfileState({
resetDefaultProfileState: {
columns: true,
rowHeight: true,
breakdownField: true,
},
})
);
}
@ -154,10 +163,12 @@ export function useEsqlMode({
// due to transformational commands, reset the associated default profile state
if (!indexPatternChanged && allColumnsChanged) {
dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: false,
breakdownField: false,
setResetDefaultProfileState({
resetDefaultProfileState: {
columns: true,
rowHeight: false,
breakdownField: false,
},
})
);
}
@ -192,5 +203,5 @@ export function useEsqlMode({
cleanup();
subscription.unsubscribe();
};
}, [dataViews, stateContainer, savedSearch, cleanup, dispatch]);
}, [dataViews, stateContainer, savedSearch, cleanup, dispatch, setResetDefaultProfileState]);
}

View file

@ -22,14 +22,20 @@ import type { DiscoverSavedSearchContainer } from './discover_saved_search_conta
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, selectCurrentTab } from './redux';
import type { InternalStateStore, TabState } from './redux';
import {
createInternalStateStore,
createRuntimeStateManager,
createTabActionInjector,
selectTab,
} from './redux';
import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context';
let history: History;
let stateStorage: IKbnUrlStateStorage;
let internalState: InternalStateStore;
let savedSearchState: DiscoverSavedSearchContainer;
let getCurrentTab: () => TabState;
describe('Test discover app state container', () => {
beforeEach(async () => {
@ -51,6 +57,8 @@ describe('Test discover app state container', () => {
globalStateContainer: getDiscoverGlobalStateContainer(stateStorage),
internalState,
});
getCurrentTab = () =>
selectTab(internalState.getState(), internalState.getState().tabs.allIds[0]);
});
const getStateContainer = () =>
@ -59,6 +67,7 @@ describe('Test discover app state container', () => {
internalState,
savedSearchContainer: savedSearchState,
services: discoverServiceMock,
injectCurrentTab: createTabActionInjector(getCurrentTab().id),
});
test('hasChanged returns whether the current state has changed', async () => {
@ -274,17 +283,13 @@ describe('Test discover app state container', () => {
describe('initAndSync', () => {
it('should call setResetDefaultProfileState correctly with no initial state', () => {
const state = getStateContainer();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -295,17 +300,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ columns: ['test'] });
const state = getStateContainer();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: true,
breakdownField: true,
@ -316,17 +317,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ rowHeight: 5 });
const state = getStateContainer();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: false,
breakdownField: true,
@ -343,17 +340,13 @@ describe('Test discover app state container', () => {
managed: false,
});
const state = getStateContainer();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(
omit(selectCurrentTab(internalState.getState()).resetDefaultProfileState, 'resetId')
).toEqual({
expect(omit(getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,

View file

@ -41,7 +41,7 @@ import {
isEsqlSource,
} from '../../../../common/data_sources';
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
import type { InternalStateStore } from './redux';
import type { InternalStateStore, TabActionInjector } from './redux';
import { internalStateActions } from './redux';
import { APP_STATE_URL_KEY } from '../../../../common';
@ -185,11 +185,13 @@ export const getDiscoverAppStateContainer = ({
internalState,
savedSearchContainer,
services,
injectCurrentTab,
}: {
stateStorage: IKbnUrlStateStorage;
internalState: InternalStateStore;
savedSearchContainer: DiscoverSavedSearchContainer;
services: DiscoverServices;
injectCurrentTab: TabActionInjector;
}): DiscoverAppStateContainer => {
let initialState = getInitialState({
initialUrlState: getCurrentUrlState(stateStorage, services),
@ -267,10 +269,12 @@ export const getDiscoverAppStateContainer = ({
// Only set default state which is not already set in the URL
internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: columns === undefined,
rowHeight: rowHeight === undefined,
breakdownField: breakdownField === undefined,
injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: columns === undefined,
rowHeight: rowHeight === undefined,
breakdownField: breakdownField === undefined,
},
})
);
}

View file

@ -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, selectCurrentTab } from './redux';
import { internalStateActions } from './redux';
jest.mock('../data_fetching/fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue({ records: [] }),
@ -178,10 +178,12 @@ describe('test getDataStateContainer', () => {
await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
stateContainer.injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: true,
rowHeight: true,
breakdownField: true,
},
})
);
@ -194,12 +196,7 @@ describe('test getDataStateContainer', () => {
await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
});
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -218,10 +215,12 @@ describe('test getDataStateContainer', () => {
await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
stateContainer.injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: false,
rowHeight: false,
breakdownField: false,
},
})
);
dataState.data$.totalHits$.next({
@ -232,12 +231,7 @@ describe('test getDataStateContainer', () => {
await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
});
expect(
omit(
selectCurrentTab(stateContainer.internalState.getState()).resetDefaultProfileState,
'resetId'
)
).toEqual({
expect(omit(stateContainer.getCurrentTab().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,

View file

@ -30,8 +30,8 @@ import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all';
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, selectCurrentTab, selectCurrentTabRuntimeState } from './redux';
import type { InternalStateStore, RuntimeStateManager, TabActionInjector, TabState } from './redux';
import { internalStateActions, selectTabRuntimeState } from './redux';
export interface SavedSearchData {
main$: DataMain$;
@ -136,6 +136,8 @@ export function getDataStateContainer({
runtimeStateManager,
getSavedSearch,
setDataView,
injectCurrentTab,
getCurrentTab,
}: {
services: DiscoverServices;
searchSessionManager: DiscoverSearchSessionManager;
@ -144,6 +146,8 @@ export function getDataStateContainer({
runtimeStateManager: RuntimeStateManager;
getSavedSearch: () => SavedSearch;
setDataView: (dataView: DataView) => void;
injectCurrentTab: TabActionInjector;
getCurrentTab: () => TabState;
}): DiscoverDataStateContainer {
const { data, uiSettings, toastNotifications, profilesManager } = services;
const { timefilter } = data.query.timefilter;
@ -251,9 +255,11 @@ export function getDataStateContainer({
}
internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: timefilter.getAbsoluteTime(),
timeRangeRelative: timefilter.getTime(),
injectCurrentTab(internalStateActions.setDataRequestParams)({
dataRequestParams: {
timeRangeAbsolute: timefilter.getAbsoluteTime(),
timeRangeRelative: timefilter.getTime(),
},
})
);
@ -263,12 +269,8 @@ export function getDataStateContainer({
query: appStateContainer.getState().query,
});
const currentInternalState = internalState.getState();
const { resetDefaultProfileState } = selectCurrentTab(currentInternalState);
const { currentDataView$ } = selectCurrentTabRuntimeState(
currentInternalState,
runtimeStateManager
);
const { id: currentTabId, resetDefaultProfileState } = getCurrentTab();
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, currentTabId);
const dataView = currentDataView$.getValue();
const defaultProfileState = dataView
? getDefaultProfileState({ profilesManager, resetDefaultProfileState, dataView })
@ -296,9 +298,9 @@ export function getDataStateContainer({
abortController,
...commonFetchDeps,
},
getCurrentTab,
async () => {
const { resetDefaultProfileState: currentResetDefaultProfileState } =
selectCurrentTab(internalState.getState());
const { resetDefaultProfileState: currentResetDefaultProfileState } = getCurrentTab();
if (currentResetDefaultProfileState.resetId !== resetDefaultProfileState.resetId) {
return;
@ -318,10 +320,12 @@ export function getDataStateContainer({
// Clear the default profile state flags after the data fetching
// is done so refetches don't reset the state again
internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: false,
rowHeight: false,
breakdownField: false,
},
})
);
}

View file

@ -9,7 +9,7 @@
import type { DiscoverStateContainer } from './discover_state';
import { createSearchSessionRestorationDataProvider } from './discover_state';
import { internalStateActions, selectCurrentTab, selectCurrentTabRuntimeState } from './redux';
import { internalStateActions, selectTabRuntimeState } from './redux';
import type { History } from 'history';
import { createBrowserHistory, createMemoryHistory } from 'history';
import { createSearchSourceMock, dataPluginMock } from '@kbn/data-plugin/public/mocks';
@ -261,12 +261,13 @@ describe('Discover state', () => {
};
const { state } = await getState('/', { savedSearch: nextSavedSearch });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -443,19 +444,19 @@ describe('Discover state', () => {
test('setDataView', async () => {
const { state, runtimeStateManager } = await getState('');
expect(
selectCurrentTabRuntimeState(
state.internalState.getState(),
runtimeStateManager
selectTabRuntimeState(
runtimeStateManager,
state.getCurrentTab().id
).currentDataView$.getValue()
).toBeUndefined();
state.actions.setDataView(dataViewMock);
expect(
selectCurrentTabRuntimeState(
state.internalState.getState(),
runtimeStateManager
selectTabRuntimeState(
runtimeStateManager,
state.getCurrentTab().id
).currentDataView$.getValue()
).toBe(dataViewMock);
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id);
expect(state.getCurrentTab().dataViewId).toBe(dataViewMock.id);
});
test('fetchData', async () => {
@ -464,12 +465,13 @@ describe('Discover state', () => {
await state.internalState.dispatch(internalStateActions.loadDataViewList());
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.LOADING);
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -492,12 +494,13 @@ describe('Discover state', () => {
const { state, getCurrentUrl } = await getState('');
await state.internalState.dispatch(internalStateActions.loadDataViewList());
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const newSavedSearch = state.savedSearchState.getState();
@ -533,12 +536,13 @@ describe('Discover state', () => {
test('loadNewSavedSearch given an empty URL using loadSavedSearch', async () => {
const { state, getCurrentUrl } = await getState('/');
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const newSavedSearch = state.savedSearchState.getState();
@ -558,12 +562,13 @@ describe('Discover state', () => {
{ isEmptyUrl: false }
);
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const newSavedSearch = state.savedSearchState.getState();
@ -583,12 +588,13 @@ describe('Discover state', () => {
{ isEmptyUrl: false }
);
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const newSavedSearch = state.savedSearchState.getState();
@ -619,12 +625,13 @@ describe('Discover state', () => {
return Promise.resolve(savedSearchWithDefaults);
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: 'the-saved-search-id',
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: 'the-saved-search-id',
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const newSavedSearch = state.savedSearchState.getState();
@ -645,12 +652,13 @@ describe('Discover state', () => {
isEmptyUrl: false,
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -675,12 +683,13 @@ describe('Discover state', () => {
isEmptyUrl: false,
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -706,12 +715,13 @@ describe('Discover state', () => {
isEmptyUrl: false,
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -737,12 +747,13 @@ describe('Discover state', () => {
isEmptyUrl: false,
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -755,12 +766,13 @@ describe('Discover state', () => {
const url = '/#?_a=(hideChart:true,columns:!(message))&_g=()';
const { state } = await getState(url, { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getState().hideChart).toBe(undefined);
@ -771,12 +783,13 @@ describe('Discover state', () => {
const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()';
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
@ -794,12 +807,13 @@ describe('Discover state', () => {
"/#?_a=(dataSource:(dataViewId:abcde,type:dataView),query:(esql:'FROM test'))&_g=()";
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.appState.getState().dataSource).toEqual(createEsqlDataSource());
@ -810,12 +824,13 @@ describe('Discover state', () => {
const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()';
const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
@ -845,12 +860,13 @@ describe('Discover state', () => {
return Promise.resolve(savedSearchWithDefaults);
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
@ -872,12 +888,13 @@ describe('Discover state', () => {
return Promise.resolve(savedSearchWithDefaults);
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: 'the-saved-search-id-with-timefield',
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: 'the-saved-search-id-with-timefield',
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe(
@ -899,13 +916,16 @@ describe('Discover state', () => {
return Promise.resolve(savedSearchWithDefaults);
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: {
dataSource: createDataViewDataSource({ dataViewId: 'index-pattern-with-timefield-id' }),
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: {
dataSource: createDataViewDataSource({
dataViewId: 'index-pattern-with-timefield-id',
}),
},
},
})
);
@ -929,12 +949,13 @@ describe('Discover state', () => {
isPersisted: () => false,
}));
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: dataViewSpecMock,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: dataViewSpecMock,
defaultUrlState: undefined,
},
})
);
expect(state.savedSearchState.getInitial$().getValue().id).toEqual(undefined);
@ -952,12 +973,13 @@ describe('Discover state', () => {
test('loadSavedSearch resetting query & filters of data service', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(discoverServiceMock.data.query.queryString.clearQuery).toHaveBeenCalled();
@ -972,12 +994,13 @@ describe('Discover state', () => {
savedSearchWithQueryAndFilters.searchSource.setField('filter', filters);
const { state } = await getState('/', { savedSearch: savedSearchWithQueryAndFilters });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(discoverServiceMock.data.query.queryString.setQuery).toHaveBeenCalledWith(query);
@ -991,12 +1014,13 @@ describe('Discover state', () => {
const adHocDataViewId = savedSearchAdHoc.searchSource.getField('index')!.id;
const { state } = await getState('/', { savedSearch: savedSearchAdHocCopy });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchAdHoc.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchAdHoc.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.appState.getState().dataSource).toEqual(
@ -1014,12 +1038,13 @@ describe('Discover state', () => {
isEmptyUrl: false,
});
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMockWithESQL.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMockWithESQL.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const nextSavedSearch = state.savedSearchState.getState();
@ -1059,12 +1084,13 @@ describe('Discover state', () => {
const { actions, savedSearchState, dataState } = state;
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
actions.initializeAndSync();
@ -1094,20 +1120,19 @@ describe('Discover state', () => {
test('onDataViewCreated - persisted data view', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
await state.actions.onDataViewCreated(dataViewComplexMock);
await waitFor(() => {
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
dataViewComplexMock.id
);
expect(state.getCurrentTab().dataViewId).toBe(dataViewComplexMock.id);
});
expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: dataViewComplexMock.id! })
@ -1121,12 +1146,13 @@ describe('Discover state', () => {
test('onDataViewCreated - ad-hoc data view', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -1137,7 +1163,7 @@ describe('Discover state', () => {
);
await state.actions.onDataViewCreated(dataViewAdHoc);
await waitFor(() => {
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewAdHoc.id);
expect(state.getCurrentTab().dataViewId).toBe(dataViewAdHoc.id);
});
expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: dataViewAdHoc.id! })
@ -1151,22 +1177,21 @@ describe('Discover state', () => {
test('onDataViewEdited - persisted data view', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
const selectedDataViewId = selectCurrentTab(state.internalState.getState()).dataViewId;
const selectedDataViewId = state.getCurrentTab().dataViewId;
expect(selectedDataViewId).toBe(dataViewMock.id);
state.actions.initializeAndSync();
await state.actions.onDataViewEdited(dataViewMock);
await waitFor(() => {
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
selectedDataViewId
);
expect(state.getCurrentTab().dataViewId).toBe(selectedDataViewId);
});
state.actions.stopSyncing();
});
@ -1178,7 +1203,7 @@ describe('Discover state', () => {
const previousId = dataViewAdHoc.id;
await state.actions.onDataViewEdited(dataViewAdHoc);
await waitFor(() => {
expect(selectCurrentTab(state.internalState.getState()).dataViewId).not.toBe(previousId);
expect(state.getCurrentTab().dataViewId).not.toBe(previousId);
});
state.actions.stopSyncing();
});
@ -1187,12 +1212,13 @@ describe('Discover state', () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
state.actions.initializeAndSync();
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.savedSearchState.update({ nextState: { hideChart: true } });
@ -1208,23 +1234,25 @@ describe('Discover state', () => {
{ savedSearch: savedSearchMock, isEmptyUrl: false }
);
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.appState.get().filters).toHaveLength(1);
history.push('/');
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: undefined,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(state.appState.get().filters).toBeUndefined();
@ -1233,12 +1261,13 @@ describe('Discover state', () => {
test('onCreateDefaultAdHocDataView', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -1254,12 +1283,13 @@ describe('Discover state', () => {
const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock });
// Load a given persisted saved search
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
state.actions.initializeAndSync();
@ -1267,7 +1297,7 @@ describe('Discover state', () => {
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(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id!);
expect(state.getCurrentTab().dataViewId).toBe(dataViewMock.id!);
// Change the data view, this should change the URL and trigger a fetch
await state.actions.onChangeDataView(dataViewComplexMock.id!);
@ -1278,9 +1308,7 @@ describe('Discover state', () => {
await waitFor(() => {
expect(state.dataState.fetch).toHaveBeenCalledTimes(1);
});
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(
dataViewComplexMock.id!
);
expect(state.getCurrentTab().dataViewId).toBe(dataViewComplexMock.id!);
// Undo all changes to the saved search, this should trigger a fetch, again
await state.actions.undoSavedSearchChanges();
@ -1289,7 +1317,7 @@ describe('Discover state', () => {
await waitFor(() => {
expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
});
expect(selectCurrentTab(state.internalState.getState()).dataViewId).toBe(dataViewMock.id!);
expect(state.getCurrentTab().dataViewId).toBe(dataViewMock.id!);
state.actions.stopSyncing();
});
@ -1308,12 +1336,13 @@ describe('Discover state', () => {
discoverServiceMock.data.query.timefilter.timefilter.setTime = setTime;
discoverServiceMock.data.query.timefilter.timefilter.setRefreshInterval = setRefreshInterval;
await state.internalState.dispatch(
internalStateActions.initializeSession({
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
state.injectCurrentTab(internalStateActions.initializeSession)({
initializeSessionParams: {
stateContainer: state,
discoverSessionId: savedSearchMock.id,
dataViewSpec: undefined,
defaultUrlState: undefined,
},
})
);
expect(setTime).toHaveBeenCalledTimes(1);

View file

@ -43,12 +43,21 @@ import {
DataSourceType,
isDataSourceType,
} from '../../../../common/data_sources';
import type { InternalStateStore, RuntimeStateManager } from './redux';
import { internalStateActions, selectCurrentTabRuntimeState } from './redux';
import type { InternalStateStore, RuntimeStateManager, TabActionInjector, TabState } from './redux';
import {
createTabActionInjector,
internalStateActions,
selectTab,
selectTabRuntimeState,
} from './redux';
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
import { getSavedSearchContainer } from './discover_saved_search_container';
export interface DiscoverStateContainerParams {
/**
* The ID of the tab associated with this state container
*/
tabId: string;
/**
* The current savedSearch
*/
@ -111,6 +120,14 @@ export interface DiscoverStateContainer {
* Internal shared state that's used at several places in the UI
*/
internalState: InternalStateStore;
/**
* Injects the current tab into a given internalState action
*/
injectCurrentTab: TabActionInjector;
/**
* Gets the state of the current tab
*/
getCurrentTab: () => TabState;
/**
* State manager for runtime state that can't be stored in Redux
*/
@ -220,6 +237,7 @@ export interface DiscoverStateContainer {
* Used to sync URL with UI state
*/
export function getDiscoverStateContainer({
tabId,
services,
customizationContext,
stateStorageContainer,
@ -228,6 +246,8 @@ export function getDiscoverStateContainer({
}: DiscoverStateContainerParams): DiscoverStateContainer {
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
const toasts = services.core.notifications.toasts;
const injectCurrentTab = createTabActionInjector(tabId);
const getCurrentTab = () => selectTab(internalState.getState(), tabId);
/**
* state storage for state in the URL
@ -271,6 +291,7 @@ export function getDiscoverStateContainer({
internalState,
savedSearchContainer,
services,
injectCurrentTab,
});
const pauseAutoRefreshInterval = async (dataView: DataView) => {
@ -286,7 +307,7 @@ export function getDiscoverStateContainer({
};
const setDataView = (dataView: DataView) => {
internalState.dispatch(internalStateActions.setDataView(dataView));
internalState.dispatch(injectCurrentTab(internalStateActions.setDataView)({ dataView }));
pauseAutoRefreshInterval(dataView);
savedSearchContainer.getState().searchSource.setField('index', dataView);
};
@ -299,6 +320,8 @@ export function getDiscoverStateContainer({
runtimeStateManager,
getSavedSearch: savedSearchContainer.getState,
setDataView,
injectCurrentTab,
getCurrentTab,
});
/**
@ -306,10 +329,7 @@ export function getDiscoverStateContainer({
* This is to prevent duplicate ids messing with our system
*/
const updateAdHocDataViewId = async () => {
const { currentDataView$ } = selectCurrentTabRuntimeState(
internalState.getState(),
runtimeStateManager
);
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, tabId);
const prevDataView = currentDataView$.getValue();
if (!prevDataView || prevDataView.isPersisted()) return;
@ -457,10 +477,7 @@ 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
);
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, tabId);
savedSearchContainer.update({
nextDataView: currentDataView$.getValue(),
nextState: appStateContainer.getState(),
@ -531,6 +548,8 @@ export function getDiscoverStateContainer({
internalState,
runtimeStateManager,
appState: appStateContainer,
injectCurrentTab,
getCurrentTab,
});
};
@ -560,7 +579,7 @@ export function getDiscoverStateContainer({
});
}
internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
internalState.dispatch(injectCurrentTab(internalStateActions.resetOnSavedSearchChange)());
await appStateContainer.replaceUrlState(newAppState);
return nextSavedSearch;
};
@ -592,6 +611,8 @@ export function getDiscoverStateContainer({
globalState: globalStateContainer,
appState: appStateContainer,
internalState,
injectCurrentTab,
getCurrentTab,
runtimeStateManager,
dataState: dataStateContainer,
savedSearchState: savedSearchContainer,

View file

@ -9,20 +9,31 @@
import type { DataView } from '@kbn/data-views-plugin/common';
import { differenceBy } from 'lodash';
import { internalStateSlice, type InternalStateThunkActionCreator } from '../internal_state';
import {
internalStateSlice,
type TabActionPayload,
type InternalStateThunkActionCreator,
} from '../internal_state';
import { selectTabRuntimeState } from '../runtime_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, getState, { runtimeStateManager }) => {
dispatch(internalStateSlice.actions.setDataViewId(dataView.id));
const { currentDataView$ } = selectCurrentTabRuntimeState(getState(), runtimeStateManager);
export const setDataView: InternalStateThunkActionCreator<
[TabActionPayload<{ dataView: DataView }>]
> =
({ tabId, dataView }) =>
(dispatch, _, { runtimeStateManager }) => {
dispatch(
internalStateSlice.actions.setDataViewId({
tabId,
dataViewId: dataView.id,
})
);
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, tabId);
currentDataView$.next(dataView);
};

View file

@ -12,7 +12,11 @@ import { isOfAggregateQueryType } from '@kbn/es-query';
import { getSavedSearchFullPathUrl } from '@kbn/saved-search-plugin/public';
import { i18n } from '@kbn/i18n';
import { cloneDeep, isEqual } from 'lodash';
import { internalStateSlice, type InternalStateThunkActionCreator } from '../internal_state';
import {
internalStateSlice,
type TabActionPayload,
type InternalStateThunkActionCreator,
} from '../internal_state';
import {
getInitialState,
type AppStateUrl,
@ -30,7 +34,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';
import { selectTabRuntimeState } from '../runtime_state';
export interface InitializeSessionParams {
stateContainer: DiscoverStateContainer;
@ -40,16 +44,19 @@ export interface InitializeSessionParams {
}
export const initializeSession: InternalStateThunkActionCreator<
[InitializeSessionParams],
[TabActionPayload<{ initializeSessionParams: InitializeSessionParams }>],
Promise<{ showNoDataPage: boolean }>
> =
({ stateContainer, discoverSessionId, dataViewSpec, defaultUrlState }) =>
({
tabId,
initializeSessionParams: { stateContainer, discoverSessionId, dataViewSpec, defaultUrlState },
}) =>
async (
dispatch,
getState,
{ services, customizationContext, runtimeStateManager, urlStateStorage }
) => {
dispatch(internalStateSlice.actions.resetOnSavedSearchChange());
dispatch(internalStateSlice.actions.resetOnSavedSearchChange({ tabId }));
/**
* "No data" checks
@ -109,7 +116,7 @@ export const initializeSession: InternalStateThunkActionCreator<
let dataView: DataView;
if (isOfAggregateQueryType(initialQuery)) {
const { currentDataView$ } = selectCurrentTabRuntimeState(getState(), runtimeStateManager);
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, tabId);
// Regardless of what was requested, we always use ad hoc data views for ES|QL
dataView = await getEsqlDataView(
@ -134,7 +141,7 @@ export const initializeSession: InternalStateThunkActionCreator<
dataView = result.dataView;
}
dispatch(setDataView(dataView));
dispatch(setDataView({ tabId, dataView }));
if (!dataView.isPersisted()) {
dispatch(appendAdHocDataViews(dataView));

View file

@ -10,7 +10,7 @@
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 { selectAllTabs, selectTab } from '../selectors';
import {
defaultTabState,
internalStateSlice,
@ -39,15 +39,16 @@ export const setTabs: InternalStateThunkActionCreator<
};
export interface UpdateTabsParams {
currentTabId: string;
updateState: TabbedContentState;
stopSyncing?: () => void;
}
export const updateTabs: InternalStateThunkActionCreator<[UpdateTabsParams], Promise<void>> =
({ updateState: { items, selectedItem }, stopSyncing }) =>
({ currentTabId, updateState: { items, selectedItem }, stopSyncing }) =>
async (dispatch, getState, { urlStateStorage }) => {
const currentState = getState();
const currentTab = selectCurrentTab(currentState);
const currentTab = selectTab(currentState, currentTabId);
let updatedTabs = items.map<TabState>((item) => {
const existingTab = currentState.tabs.byId[item.id];
return existingTab ? { ...existingTab, ...item } : { ...defaultTabState, ...item };
@ -77,10 +78,5 @@ export const updateTabs: InternalStateThunkActionCreator<[UpdateTabsParams], Pro
}
}
dispatch(
setTabs({
allTabs: updatedTabs,
selectedTabId: selectedItem?.id ?? currentTab.id,
})
);
dispatch(setTabs({ allTabs: updatedTabs }));
};

View file

@ -18,8 +18,13 @@ import {
import React, { type PropsWithChildren, useMemo, createContext } from 'react';
import { useAdHocDataViews } from './runtime_state';
import type { DiscoverInternalState, TabState } from './types';
import { type InternalStateDispatch, type InternalStateStore } from './internal_state';
import { selectCurrentTab } from './selectors';
import {
type TabActionPayload,
type InternalStateDispatch,
type InternalStateStore,
} from './internal_state';
import { selectTab } from './selectors';
import { type TabActionInjector, createTabActionInjector } from './utils';
const internalStateContext = createContext<ReactReduxContextValue>(
// Recommended approach for versions of Redux prior to v9:
@ -42,8 +47,46 @@ export const useInternalStateDispatch: () => InternalStateDispatch =
export const useInternalStateSelector: TypedUseSelectorHook<DiscoverInternalState> =
createSelectorHook(internalStateContext);
export const useCurrentTabSelector: TypedUseSelectorHook<TabState> = (selector) =>
selector(useInternalStateSelector(selectCurrentTab));
interface CurrentTabContextValue {
currentTabId: string;
injectCurrentTab: TabActionInjector;
}
const currentTabContext = createContext<CurrentTabContextValue | undefined>(undefined);
export const CurrentTabProvider = ({
currentTabId,
children,
}: PropsWithChildren<{ currentTabId: string }>) => {
const contextValue = useMemo<CurrentTabContextValue>(
() => ({ currentTabId, injectCurrentTab: createTabActionInjector(currentTabId) }),
[currentTabId]
);
return <currentTabContext.Provider value={contextValue}>{children}</currentTabContext.Provider>;
};
export const useCurrentTabContext = () => {
const context = React.useContext(currentTabContext);
if (!context) {
throw new Error('useCurrentTabContext must be used within a CurrentTabProvider');
}
return context;
};
export const useCurrentTabSelector: TypedUseSelectorHook<TabState> = (selector) => {
const { currentTabId } = useCurrentTabContext();
return useInternalStateSelector((state) => selector(selectTab(state, currentTabId)));
};
export const useCurrentTabAction = <TPayload extends TabActionPayload, TReturn>(
actionCreator: (params: TPayload) => TReturn
) => {
const { injectCurrentTab } = useCurrentTabContext();
return useMemo(() => injectCurrentTab(actionCreator), [actionCreator, injectCurrentTab]);
};
export const useDataViewsForPicker = () => {
const originalAdHocDataViews = useAdHocDataViews();

View file

@ -23,7 +23,7 @@ import {
export type { DiscoverInternalState, TabState, InternalStateDataRequestParams } from './types';
export { type InternalStateStore, createInternalStateStore, createTabItem } from './internal_state';
export { type InternalStateStore, createInternalStateStore } from './internal_state';
export const internalStateActions = {
...omit(
@ -47,18 +47,23 @@ export {
InternalStateProvider,
useInternalStateDispatch,
useInternalStateSelector,
CurrentTabProvider,
useCurrentTabSelector,
useCurrentTabAction,
useDataViewsForPicker,
} from './hooks';
export { selectAllTabs, selectCurrentTab } from './selectors';
export { selectAllTabs, selectTab } from './selectors';
export {
type RuntimeStateManager,
createRuntimeStateManager,
useRuntimeState,
selectCurrentTabRuntimeState,
selectTabRuntimeState,
useCurrentTabRuntimeState,
RuntimeStateProvider,
useCurrentDataView,
useAdHocDataViews,
} from './runtime_state';
export { type TabActionInjector, createTabActionInjector, createTabItem } from './utils';

View file

@ -12,8 +12,8 @@ import {
createInternalStateStore,
createRuntimeStateManager,
internalStateActions,
selectCurrentTab,
selectCurrentTabRuntimeState,
selectTab,
selectTabRuntimeState,
} from '.';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { mockCustomizationContext } from '../../../../customizations/__mocks__/customization_context';
@ -28,14 +28,15 @@ describe('InternalStateStore', () => {
runtimeStateManager,
urlStateStorage: createKbnUrlStateStorage(),
});
expect(selectCurrentTab(store.getState()).dataViewId).toBeUndefined();
const tabId = store.getState().tabs.allIds[0];
expect(selectTab(store.getState(), tabId).dataViewId).toBeUndefined();
expect(
selectCurrentTabRuntimeState(store.getState(), runtimeStateManager).currentDataView$.value
selectTabRuntimeState(runtimeStateManager, tabId).currentDataView$.value
).toBeUndefined();
store.dispatch(internalStateActions.setDataView(dataViewMock));
expect(selectCurrentTab(store.getState()).dataViewId).toBe(dataViewMock.id);
expect(
selectCurrentTabRuntimeState(store.getState(), runtimeStateManager).currentDataView$.value
).toBe(dataViewMock);
store.dispatch(internalStateActions.setDataView({ tabId, dataView: dataViewMock }));
expect(selectTab(store.getState(), tabId).dataViewId).toBe(dataViewMock.id);
expect(selectTabRuntimeState(runtimeStateManager, tabId).currentDataView$.value).toBe(
dataViewMock
);
});
});

View file

@ -17,7 +17,6 @@ import {
type ThunkDispatch,
} 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';
@ -29,12 +28,8 @@ import {
type TabState,
} from './types';
import { loadDataViewList, setTabs } from './actions';
import { selectAllTabs, selectCurrentTab } from './selectors';
const DEFAULT_TAB_LABEL = i18n.translate('discover.defaultTabLabel', {
defaultMessage: 'Untitled session',
});
const DEFAULT_TAB_REGEX = new RegExp(`^${DEFAULT_TAB_LABEL}( \\d+)?$`);
import { selectAllTabs } from './selectors';
import { createTabItem } from './utils';
export const defaultTabState: Omit<TabState, keyof TabItem> = {
dataViewId: undefined,
@ -67,23 +62,22 @@ const initialState: DiscoverInternalState = {
savedDataViews: [],
expandedDoc: undefined,
isESQLToDataViewTransitionModalVisible: false,
tabs: { byId: {}, allIds: [], currentId: '' },
tabs: { byId: {}, allIds: [] },
};
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;
export type TabActionPayload<T extends { [key: string]: unknown } = {}> = { tabId: string } & T;
return { id, label };
};
type TabAction<T extends { [key: string]: unknown } = {}> = PayloadAction<TabActionPayload<T>>;
const withCurrentTab = (state: DiscoverInternalState, fn: (tab: TabState) => void) => {
const currentTab = selectCurrentTab(state);
const withTab = <TAction extends TabAction>(
state: DiscoverInternalState,
action: TAction,
fn: (tab: TabState) => void
) => {
const tab = state.tabs.byId[action.payload.tabId];
if (currentTab) {
fn(currentTab);
if (tab) {
fn(tab);
}
};
@ -98,7 +92,7 @@ export const internalStateSlice = createSlice({
state.initializationState = action.payload;
},
setTabs: (state, action: PayloadAction<{ allTabs: TabState[]; selectedTabId: string }>) => {
setTabs: (state, action: PayloadAction<{ allTabs: TabState[] }>) => {
state.tabs.byId = action.payload.allTabs.reduce<Record<string, TabState>>(
(acc, tab) => ({
...acc,
@ -107,21 +101,20 @@ export const internalStateSlice = createSlice({
{}
);
state.tabs.allIds = action.payload.allTabs.map((tab) => tab.id);
state.tabs.currentId = action.payload.selectedTabId;
},
setDataViewId: (state, action: PayloadAction<string | undefined>) =>
withCurrentTab(state, (tab) => {
if (action.payload !== tab.dataViewId) {
setDataViewId: (state, action: TabAction<{ dataViewId: string | undefined }>) =>
withTab(state, action, (tab) => {
if (action.payload.dataViewId !== tab.dataViewId) {
state.expandedDoc = undefined;
}
tab.dataViewId = action.payload;
tab.dataViewId = action.payload.dataViewId;
}),
setIsDataViewLoading: (state, action: PayloadAction<boolean>) =>
withCurrentTab(state, (tab) => {
tab.isDataViewLoading = action.payload;
setIsDataViewLoading: (state, action: TabAction<{ isDataViewLoading: boolean }>) =>
withTab(state, action, (tab) => {
tab.isDataViewLoading = action.payload.isDataViewLoading;
}),
setDefaultProfileAdHocDataViewIds: (state, action: PayloadAction<string[]>) => {
@ -132,17 +125,23 @@ export const internalStateSlice = createSlice({
state.expandedDoc = action.payload;
},
setDataRequestParams: (state, action: PayloadAction<InternalStateDataRequestParams>) =>
withCurrentTab(state, (tab) => {
tab.dataRequestParams = action.payload;
setDataRequestParams: (
state,
action: TabAction<{ dataRequestParams: InternalStateDataRequestParams }>
) =>
withTab(state, action, (tab) => {
tab.dataRequestParams = action.payload.dataRequestParams;
}),
setOverriddenVisContextAfterInvalidation: (
state,
action: PayloadAction<TabState['overriddenVisContextAfterInvalidation']>
action: TabAction<{
overriddenVisContextAfterInvalidation: TabState['overriddenVisContextAfterInvalidation'];
}>
) =>
withCurrentTab(state, (tab) => {
tab.overriddenVisContextAfterInvalidation = action.payload;
withTab(state, action, (tab) => {
tab.overriddenVisContextAfterInvalidation =
action.payload.overriddenVisContextAfterInvalidation;
}),
setIsESQLToDataViewTransitionModalVisible: (state, action: PayloadAction<boolean>) => {
@ -151,26 +150,32 @@ export const internalStateSlice = createSlice({
setResetDefaultProfileState: {
prepare: (
resetDefaultProfileState: Omit<TabState['resetDefaultProfileState'], 'resetId'>
payload: TabActionPayload<{
resetDefaultProfileState: Omit<TabState['resetDefaultProfileState'], 'resetId'>;
}>
) => ({
payload: {
...resetDefaultProfileState,
resetId: uuidv4(),
...payload,
resetDefaultProfileState: {
...payload.resetDefaultProfileState,
resetId: uuidv4(),
},
},
}),
reducer: (state, action: PayloadAction<TabState['resetDefaultProfileState']>) =>
withCurrentTab(state, (tab) => {
tab.resetDefaultProfileState = action.payload;
reducer: (
state,
action: TabAction<{ resetDefaultProfileState: TabState['resetDefaultProfileState'] }>
) =>
withTab(state, action, (tab) => {
tab.resetDefaultProfileState = action.payload.resetDefaultProfileState;
}),
},
resetOnSavedSearchChange: (state) => {
withCurrentTab(state, (tab) => {
resetOnSavedSearchChange: (state, action: TabAction) =>
withTab(state, action, (tab) => {
tab.overriddenVisContextAfterInvalidation = undefined;
});
state.expandedDoc = undefined;
},
state.expandedDoc = undefined;
}),
},
extraReducers: (builder) => {
builder.addCase(loadDataViewList.fulfilled, (state, action) => {
@ -198,7 +203,7 @@ export const createInternalStateStore = (options: InternalStateThunkDependencies
...defaultTabState,
...createTabItem(selectAllTabs(store.getState())),
};
store.dispatch(setTabs({ allTabs: [defaultTab], selectedTabId: defaultTab.id }));
store.dispatch(setTabs({ allTabs: [defaultTab] }));
return store;
};

View file

@ -11,8 +11,7 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { BehaviorSubject } from 'rxjs';
import { useInternalStateSelector } from './hooks';
import type { DiscoverInternalState } from './types';
import { useCurrentTabContext } from './hooks';
interface DiscoverRuntimeState {
adHocDataViews: DataView[];
@ -46,22 +45,15 @@ export const createTabRuntimeState = (): ReactiveTabRuntimeState => ({
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 selectTabRuntimeState = (runtimeStateManager: RuntimeStateManager, tabId: string) =>
runtimeStateManager.tabs.byId[tabId];
export const useCurrentTabRuntimeState = <T,>(
runtimeStateManager: RuntimeStateManager,
selector: (tab: ReactiveTabRuntimeState) => BehaviorSubject<T>
) => {
const tab = useInternalStateSelector((state) =>
selectCurrentTabRuntimeState(state, runtimeStateManager)
);
return useRuntimeState(selector(tab));
const { currentTabId } = useCurrentTabContext();
return useRuntimeState(selector(selectTabRuntimeState(runtimeStateManager, currentTabId)));
};
type CombinedRuntimeState = DiscoverRuntimeState & TabRuntimeState;

View file

@ -12,5 +12,4 @@ 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];
export const selectTab = (state: DiscoverInternalState, tabId: string) => state.tabs.byId[tabId];

View file

@ -70,6 +70,5 @@ export interface DiscoverInternalState {
tabs: {
byId: Record<string, TabState>;
allIds: string[];
currentId: string;
};
}

View file

@ -7,9 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { v4 as uuid } from 'uuid';
import { i18n } from '@kbn/i18n';
import type { TabItem } from '@kbn/unified-tabs';
import { createAsyncThunk } from '@reduxjs/toolkit';
import type { DiscoverInternalState } from './types';
import type { InternalStateDispatch, InternalStateThunkDependencies } from './internal_state';
import type { DiscoverInternalState, TabState } from './types';
import type {
InternalStateDispatch,
InternalStateThunkDependencies,
TabActionPayload,
} 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.
@ -23,3 +30,29 @@ type CreateInternalStateAsyncThunk = ReturnType<
export const createInternalStateAsyncThunk: CreateInternalStateAsyncThunk =
createAsyncThunk.withTypes();
type WithoutTabId<TPayload extends TabActionPayload> = Omit<TPayload, 'tabId'>;
type VoidIfEmpty<T> = keyof T extends never ? void : T;
export const createTabActionInjector =
(tabId: string) =>
<TPayload extends TabActionPayload, TReturn>(actionCreator: (params: TPayload) => TReturn) =>
(payload: VoidIfEmpty<WithoutTabId<TPayload>>) => {
return actionCreator({ ...(payload ?? {}), tabId } as TPayload);
};
export type TabActionInjector = ReturnType<typeof createTabActionInjector>;
const DEFAULT_TAB_LABEL = i18n.translate('discover.defaultTabLabel', {
defaultMessage: 'Untitled session',
});
const DEFAULT_TAB_REGEX = new RegExp(`^${DEFAULT_TAB_LABEL}( \\d+)?$`);
export const createTabItem = (allTabs: TabState[]): TabItem => {
const id = uuid();
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 };
};

View file

@ -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, selectCurrentTab } from '../redux';
import { createRuntimeStateManager, internalStateActions } from '../redux';
const setupTestParams = (dataView: DataView | undefined) => {
const savedSearch = savedSearchMock;
@ -25,7 +25,9 @@ const setupTestParams = (dataView: DataView | undefined) => {
const runtimeStateManager = createRuntimeStateManager();
const discoverState = getDiscoverStateMock({ savedSearch, runtimeStateManager });
discoverState.internalState.dispatch(
internalStateActions.setDataView(savedSearch.searchSource.getField('index')!)
discoverState.injectCurrentTab(internalStateActions.setDataView)({
dataView: savedSearch.searchSource.getField('index')!,
})
);
services.dataViews.get = jest.fn(() => Promise.resolve(dataView as DataView));
discoverState.appState.update = jest.fn();
@ -34,6 +36,8 @@ const setupTestParams = (dataView: DataView | undefined) => {
appState: discoverState.appState,
internalState: discoverState.internalState,
runtimeStateManager,
injectCurrentTab: discoverState.injectCurrentTab,
getCurrentTab: discoverState.getCurrentTab,
};
};
@ -41,41 +45,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(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
expect(params.getCurrentTab().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(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
expect(params.getCurrentTab().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(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
expect(params.getCurrentTab().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(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
expect(params.getCurrentTab().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(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(true);
expect(params.getCurrentTab().isDataViewLoading).toBe(true);
await promise;
expect(params.appState.update).not.toHaveBeenCalled();
expect(selectCurrentTab(params.internalState.getState()).isDataViewLoading).toBe(false);
expect(params.getCurrentTab().isDataViewLoading).toBe(false);
});
it('should call setResetDefaultProfileState correctly when switching data view', async () => {
const params = setupTestParams(dataViewComplexMock);
expect(selectCurrentTab(params.internalState.getState()).resetDefaultProfileState).toEqual(
expect(params.getCurrentTab().resetDefaultProfileState).toEqual(
expect.objectContaining({
columns: false,
rowHeight: false,
@ -83,7 +87,7 @@ describe('changeDataView', () => {
})
);
await changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
expect(selectCurrentTab(params.internalState.getState()).resetDefaultProfileState).toEqual(
expect(params.getCurrentTab().resetDefaultProfileState).toEqual(
expect.objectContaining({
columns: true,
rowHeight: true,

View file

@ -20,9 +20,11 @@ import type { DiscoverServices } from '../../../../build_services';
import { getDataViewAppState } from './get_switch_data_view_app_state';
import {
internalStateActions,
selectCurrentTabRuntimeState,
selectTabRuntimeState,
type InternalStateStore,
type RuntimeStateManager,
type TabActionInjector,
type TabState,
} from '../redux';
/**
@ -34,25 +36,29 @@ export async function changeDataView({
internalState,
runtimeStateManager,
appState,
injectCurrentTab,
getCurrentTab,
}: {
dataViewId: string | DataView;
services: DiscoverServices;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
appState: DiscoverAppStateContainer;
injectCurrentTab: TabActionInjector;
getCurrentTab: () => TabState;
}) {
addLog('[ui] changeDataView', { id: dataViewId });
const { dataViews, uiSettings } = services;
const { currentDataView$ } = selectCurrentTabRuntimeState(
internalState.getState(),
runtimeStateManager
);
const { id: currentTabId } = getCurrentTab();
const { currentDataView$ } = selectTabRuntimeState(runtimeStateManager, currentTabId);
const currentDataView = currentDataView$.getValue();
const state = appState.getState();
let nextDataView: DataView | null = null;
internalState.dispatch(internalStateActions.setIsDataViewLoading(true));
internalState.dispatch(
injectCurrentTab(internalStateActions.setIsDataViewLoading)({ isDataViewLoading: true })
);
try {
nextDataView =
@ -71,10 +77,12 @@ export async function changeDataView({
if (nextDataView && currentDataView) {
// Reset the default profile state if we are switching to a different data view
internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
resetDefaultProfileState: {
columns: true,
rowHeight: true,
breakdownField: true,
},
})
);
@ -96,5 +104,7 @@ export async function changeDataView({
}
}
internalState.dispatch(internalStateActions.setIsDataViewLoading(false));
internalState.dispatch(
injectCurrentTab(internalStateActions.setIsDataViewLoading)({ isDataViewLoading: false })
);
}