[Discover] Replace DiscoverInternalStateContainer with Redux based InternalStateStore (#208784)

## Summary

This PR replaces Discover's current `DiscoverInternalStateContainer`
(based on Kibana's custom `ReduxLikeStateContainer`) with an actual
Redux store using Redux Toolkit. It's the first step toward migrating
all of Discover's state management to Redux as part of the Discover tabs
project.

Part of #210160.
Resolves #213304.

### 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-06 17:08:58 -04:00 committed by GitHub
parent 989cf1ec34
commit ccae358d37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1209 additions and 876 deletions

View file

@ -241,9 +241,9 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
>(); >();
const stateStorage = stateContainer.stateStorage; const stateStorage = stateContainer.stateStorage;
const dataView = useObservable( const dataView = useObservable(
stateContainer.internalState.state$, stateContainer.runtimeStateManager.currentDataView$,
stateContainer.internalState.getState() stateContainer.runtimeStateManager.currentDataView$.getValue()
).dataView; );
useEffect(() => { useEffect(() => {
if (!controlGroupAPI) { if (!controlGroupAPI) {
@ -262,7 +262,6 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
}); });
const filterSubscription = controlGroupAPI.filters$.subscribe((newFilters = []) => { const filterSubscription = controlGroupAPI.filters$.subscribe((newFilters = []) => {
stateContainer.internalState.transitions.setCustomFilters(newFilters);
stateContainer.actions.fetchData(); stateContainer.actions.fetchData();
}); });

View file

@ -79,12 +79,16 @@ export const deepMockedFields = shallowMockedFields.map(
) as DataView['fields']; ) as DataView['fields'];
export const buildDataViewMock = ({ export const buildDataViewMock = ({
name, id,
fields: definedFields, title,
name = 'data-view-mock',
fields: definedFields = [] as unknown as DataView['fields'],
timeFieldName, timeFieldName,
}: { }: {
name: string; id?: string;
fields: DataView['fields']; title?: string;
name?: string;
fields?: DataView['fields'];
timeFieldName?: string; timeFieldName?: string;
}): DataView => { }): DataView => {
const dataViewFields = [...definedFields] as DataView['fields']; const dataViewFields = [...definedFields] as DataView['fields'];
@ -105,9 +109,12 @@ export const buildDataViewMock = ({
return new DataViewField(spec); return new DataViewField(spec);
}; };
id = id ?? `${name}-id`;
title = title ?? `${name}-title`;
const dataView = { const dataView = {
id: `${name}-id`, id,
title: `${name}-title`, title,
name, name,
metaFields: ['_index', '_score'], metaFields: ['_index', '_score'],
fields: dataViewFields, fields: dataViewFields,
@ -122,7 +129,7 @@ export const buildDataViewMock = ({
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
isTimeNanosBased: () => false, isTimeNanosBased: () => false,
isPersisted: () => true, isPersisted: () => true,
toSpec: () => ({}), toSpec: () => ({ id, title, name }),
toMinimalSpec: () => ({}), toMinimalSpec: () => ({}),
getTimeField: () => { getTimeField: () => {
return dataViewFields.find((field) => field.name === timeFieldName); return dataViewFields.find((field) => field.name === timeFieldName);

View file

@ -13,13 +13,19 @@ import { savedSearchMockWithTimeField, savedSearchMock } from './saved_search';
import { discoverServiceMock } from './services'; import { discoverServiceMock } from './services';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { mockCustomizationContext } from '../customizations/__mocks__/customization_context'; import { mockCustomizationContext } from '../customizations/__mocks__/customization_context';
import {
RuntimeStateManager,
createRuntimeStateManager,
} from '../application/main/state_management/redux';
export function getDiscoverStateMock({ export function getDiscoverStateMock({
isTimeBased = true, isTimeBased = true,
savedSearch, savedSearch,
runtimeStateManager,
}: { }: {
isTimeBased?: boolean; isTimeBased?: boolean;
savedSearch?: SavedSearch; savedSearch?: SavedSearch;
runtimeStateManager?: RuntimeStateManager;
}) { }) {
const history = createBrowserHistory(); const history = createBrowserHistory();
history.push('/'); history.push('/');
@ -27,6 +33,7 @@ export function getDiscoverStateMock({
services: discoverServiceMock, services: discoverServiceMock,
history, history,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
runtimeStateManager: runtimeStateManager ?? createRuntimeStateManager(),
}); });
container.savedSearchState.set( container.savedSearchState.set(
savedSearch ? savedSearch : isTimeBased ? savedSearchMockWithTimeField : savedSearchMock savedSearch ? savedSearch : isTimeBased ? savedSearchMockWithTimeField : savedSearchMock

View file

@ -28,6 +28,7 @@ import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../.
import { createCustomizationService } from '../../../../customizations/customization_service'; import { createCustomizationService } from '../../../../customizations/customization_service';
import { DiscoverGrid } from '../../../../components/discover_grid'; import { DiscoverGrid } from '../../../../components/discover_grid';
import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../../common/data_sources';
import { internalStateActions } from '../../state_management/redux';
const customisationService = createCustomizationService(); const customisationService = createCustomizationService();
@ -46,16 +47,18 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
stateContainer.appState.update({ stateContainer.appState.update({
dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
}); });
stateContainer.internalState.transitions.setDataRequestParams({ stateContainer.internalState.dispatch(
timeRangeRelative: { internalStateActions.setDataRequestParams({
from: '2020-05-14T11:05:13.590', timeRangeRelative: {
to: '2020-05-14T11:20:13.590', from: '2020-05-14T11:05:13.590',
}, to: '2020-05-14T11:20:13.590',
timeRangeAbsolute: { },
from: '2020-05-14T11:05:13.590', timeRangeAbsolute: {
to: '2020-05-14T11:20:13.590', from: '2020-05-14T11:05:13.590',
}, to: '2020-05-14T11:20:13.590',
}); },
})
);
stateContainer.dataState.data$.documents$ = documents$; stateContainer.dataState.data$.documents$ = documents$;

View file

@ -48,7 +48,6 @@ import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { useQuerySubscriber } from '@kbn/unified-field-list'; import { useQuerySubscriber } from '@kbn/unified-field-list';
import { DiscoverGrid } from '../../../../components/discover_grid'; import { DiscoverGrid } from '../../../../components/discover_grid';
import { getDefaultRowsPerPage } from '../../../../../common/constants'; import { getDefaultRowsPerPage } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { useAppStateSelector } from '../../state_management/discover_app_state_container'; import { useAppStateSelector } from '../../state_management/discover_app_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { FetchStatus } from '../../../types'; import { FetchStatus } from '../../../types';
@ -73,6 +72,11 @@ import {
useAdditionalCellActions, useAdditionalCellActions,
useProfileAccessor, useProfileAccessor,
} from '../../../../context_awareness'; } from '../../../../context_awareness';
import {
internalStateActions,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
const containerStyles = css` const containerStyles = css`
position: relative; position: relative;
@ -108,6 +112,7 @@ function DiscoverDocumentsComponent({
onFieldEdited?: () => void; onFieldEdited?: () => void;
}) { }) {
const services = useDiscoverServices(); const services = useDiscoverServices();
const dispatch = useInternalStateDispatch();
const documents$ = stateContainer.dataState.data$.documents$; const documents$ = stateContainer.dataState.data$.documents$;
const savedSearch = useSavedSearchInitial(); const savedSearch = useSavedSearchInitial();
const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services; const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services;
@ -204,9 +209,9 @@ function DiscoverDocumentsComponent({
const setExpandedDoc = useCallback( const setExpandedDoc = useCallback(
(doc: DataTableRecord | undefined) => { (doc: DataTableRecord | undefined) => {
stateContainer.internalState.transitions.setExpandedDoc(doc); dispatch(internalStateActions.setExpandedDoc(doc));
}, },
[stateContainer] [dispatch]
); );
const onResizeDataGrid = useCallback<NonNullable<UnifiedDataTableProps['onResize']>>( const onResizeDataGrid = useCallback<NonNullable<UnifiedDataTableProps['onResize']>>(

View file

@ -34,6 +34,7 @@ import { DiscoverMainProvider } from '../../state_management/discover_state_prov
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { PanelsToggle } from '../../../../components/panels_toggle'; import { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../../common/data_sources';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
function getStateContainer(savedSearch?: SavedSearch) { function getStateContainer(savedSearch?: SavedSearch) {
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch }); const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
@ -46,17 +47,19 @@ function getStateContainer(savedSearch?: SavedSearch) {
stateContainer.appState.update(appState); stateContainer.appState.update(appState);
stateContainer.internalState.transitions.setDataView(dataView); stateContainer.internalState.dispatch(internalStateActions.setDataView(dataView));
stateContainer.internalState.transitions.setDataRequestParams({ stateContainer.internalState.dispatch(
timeRangeAbsolute: { internalStateActions.setDataRequestParams({
from: '2020-05-14T11:05:13.590', timeRangeAbsolute: {
to: '2020-05-14T11:20:13.590', from: '2020-05-14T11:05:13.590',
}, to: '2020-05-14T11:20:13.590',
timeRangeRelative: { },
from: '2020-05-14T11:05:13.590', timeRangeRelative: {
to: '2020-05-14T11:20:13.590', from: '2020-05-14T11:05:13.590',
}, to: '2020-05-14T11:20:13.590',
}); },
})
);
return stateContainer; return stateContainer;
} }
@ -142,7 +145,9 @@ const mountComponent = async ({
<KibanaRenderContextProvider {...services.core}> <KibanaRenderContextProvider {...services.core}>
<KibanaContextProvider services={services}> <KibanaContextProvider services={services}>
<DiscoverMainProvider value={stateContainer}> <DiscoverMainProvider value={stateContainer}>
<DiscoverHistogramLayout {...props} /> <RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<DiscoverHistogramLayout {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
</KibanaContextProvider> </KibanaContextProvider>
</KibanaRenderContextProvider> </KibanaRenderContextProvider>

View file

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

View file

@ -34,7 +34,6 @@ import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider'; import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
import { DiscoverStateContainer } from '../../state_management/discover_state'; import { DiscoverStateContainer } from '../../state_management/discover_state';
import { VIEW_MODE } from '../../../../../common/constants'; import { VIEW_MODE } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { useAppStateSelector } from '../../state_management/discover_app_state_container'; import { useAppStateSelector } from '../../state_management/discover_app_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverNoResults } from '../no_results'; import { DiscoverNoResults } from '../no_results';
@ -54,6 +53,7 @@ import { DiscoverResizableLayout } from './discover_resizable_layout';
import { PanelsToggle, PanelsToggleProps } from '../../../../components/panels_toggle'; import { PanelsToggle, PanelsToggleProps } from '../../../../components/panels_toggle';
import { sendErrorMsg } from '../../hooks/use_saved_search_messages'; import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useCurrentDataView, useInternalStateSelector } from '../../state_management/redux';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav); const TopNavMemoized = React.memo(DiscoverTopNav);
@ -89,7 +89,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
state.grid, state.grid,
]); ]);
const isEsqlMode = useIsEsqlMode(); const isEsqlMode = useIsEsqlMode();
const viewMode: VIEW_MODE = useAppStateSelector((state) => { const viewMode: VIEW_MODE = useAppStateSelector((state) => {
const fieldStatsNotAvailable = const fieldStatsNotAvailable =
!uiSettings.get(SHOW_FIELD_STATISTICS) && !!dataVisualizerService; !uiSettings.get(SHOW_FIELD_STATISTICS) && !!dataVisualizerService;
@ -98,15 +97,10 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
} }
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL; return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
}); });
const [dataView, dataViewLoading] = useInternalStateSelector((state) => [ const dataView = useCurrentDataView();
state.dataView!, const dataViewLoading = useInternalStateSelector((state) => state.isDataViewLoading);
state.isDataViewLoading,
]);
const customFilters = useInternalStateSelector((state) => state.customFilters);
const dataState: DataMainMsg = useDataState(main$); const dataState: DataMainMsg = useDataState(main$);
const savedSearch = useSavedSearchInitial(); const savedSearch = useSavedSearchInitial();
const fetchCounter = useRef<number>(0); const fetchCounter = useRef<number>(0);
useEffect(() => { useEffect(() => {
@ -197,21 +191,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
[filterManager, dataView, dataViews, trackUiMetric, capabilities, ebtManager, fieldsMetadata] [filterManager, dataView, dataViews, trackUiMetric, capabilities, ebtManager, fieldsMetadata]
); );
const getOperator = (fieldName: string, values: unknown, operation: '+' | '-') => {
if (fieldName === '_exists_') {
return 'is_not_null';
}
if (values == null && operation === '-') {
return 'is_not_null';
}
if (values == null && operation === '+') {
return 'is_null';
}
return operation;
};
const onPopulateWhereClause = useCallback<DocViewFilterFn>( const onPopulateWhereClause = useCallback<DocViewFilterFn>(
(field, values, operation) => { (field, values, operation) => {
if (!field || !isOfAggregateQueryType(query)) { if (!field || !isOfAggregateQueryType(query)) {
@ -430,7 +409,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sidebarToggleState$={sidebarToggleState$} sidebarToggleState$={sidebarToggleState$}
sidebarPanel={ sidebarPanel={
<SidebarMemoized <SidebarMemoized
additionalFilters={customFilters}
columns={currentColumns} columns={currentColumns}
documents$={stateContainer.dataState.data$.documents$} documents$={stateContainer.dataState.data$.documents$}
onAddBreakdownField={canSetBreakdownField ? onAddBreakdownField : undefined} onAddBreakdownField={canSetBreakdownField ? onAddBreakdownField : undefined}
@ -503,3 +481,18 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
</EuiPage> </EuiPage>
); );
} }
const getOperator = (fieldName: string, values: unknown, operation: '+' | '-') => {
if (fieldName === '_exists_') {
return 'is_not_null';
}
if (values == null && operation === '-') {
return 'is_not_null';
}
if (values == null && operation === '+') {
return 'is_null';
}
return operation;
};

View file

@ -29,6 +29,8 @@ import type { InspectorAdapters } from '../../hooks/use_inspector';
import { UnifiedHistogramCustomization } from '../../../../customizations/customization_types/histogram_customization'; import { UnifiedHistogramCustomization } from '../../../../customizations/customization_types/histogram_customization';
import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverCustomization } from '../../../../customizations';
import { DiscoverCustomizationId } from '../../../../customizations/customization_service'; import { DiscoverCustomizationId } from '../../../../customizations/customization_service';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
import { dataViewMockWithTimeField } from '@kbn/discover-utils/src/__mocks__';
const mockData = dataPluginMock.createStartContract(); const mockData = dataPluginMock.createStartContract();
let mockQueryState = { let mockQueryState = {
@ -121,7 +123,11 @@ describe('useDiscoverHistogram', () => {
}; };
const Wrapper = ({ children }: React.PropsWithChildren<unknown>) => ( const Wrapper = ({ children }: React.PropsWithChildren<unknown>) => (
<DiscoverMainProvider value={stateContainer}>{children as ReactElement}</DiscoverMainProvider> <DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMockWithTimeField} adHocDataViews={[]}>
{children as ReactElement}
</RuntimeStateProvider>
</DiscoverMainProvider>
); );
const hook = renderHook( const hook = renderHook(
@ -379,15 +385,17 @@ describe('useDiscoverHistogram', () => {
expect(hook.result.current.isChartLoading).toBe(true); expect(hook.result.current.isChartLoading).toBe(true);
}); });
it('should use timerange + timeRangeRelative + query given by the internalState container', async () => { it('should use timerange + timeRangeRelative + query given by the internalState', async () => {
const fetch$ = new Subject<void>(); const fetch$ = new Subject<void>();
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
const timeRangeRel = { from: 'now-15m', to: 'now' }; const timeRangeRel = { from: 'now-15m', to: 'now' };
stateContainer.internalState.transitions.setDataRequestParams({ stateContainer.internalState.dispatch(
timeRangeAbsolute: timeRangeAbs, internalStateActions.setDataRequestParams({
timeRangeRelative: timeRangeRel, timeRangeAbsolute: timeRangeAbs,
}); timeRangeRelative: timeRangeRel,
})
);
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram({ stateContainer });
act(() => { act(() => {
fetch$.next(); fetch$.next();

View file

@ -44,7 +44,6 @@ import type { InspectorAdapters } from '../../hooks/use_inspector';
import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages';
import type { DiscoverStateContainer } from '../../state_management/discover_state'; import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { addLog } from '../../../../utils/add_log'; import { addLog } from '../../../../utils/add_log';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { import {
useAppStateSelector, useAppStateSelector,
type DiscoverAppState, type DiscoverAppState,
@ -52,6 +51,12 @@ import {
import { DataDocumentsMsg } from '../../state_management/discover_data_state_container'; import { DataDocumentsMsg } from '../../state_management/discover_data_state_container';
import { useSavedSearch } from '../../state_management/discover_state_provider'; import { useSavedSearch } from '../../state_management/discover_state_provider';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import {
internalStateActions,
useCurrentDataView,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
const EMPTY_ESQL_COLUMNS: DatatableColumn[] = []; const EMPTY_ESQL_COLUMNS: DatatableColumn[] = [];
const EMPTY_FILTERS: Filter[] = []; const EMPTY_FILTERS: Filter[] = [];
@ -220,7 +225,6 @@ export const useDiscoverHistogram = ({
*/ */
const { query, filters } = useQuerySubscriber({ data: services.data }); const { query, filters } = useQuerySubscriber({ data: services.data });
const requestParams = useInternalStateSelector((state) => state.dataRequestParams); const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
const customFilters = useInternalStateSelector((state) => state.customFilters);
const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams; const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams;
// When in ES|QL mode, update the data view, query, and // When in ES|QL mode, update the data view, query, and
// columns only when documents are done fetching so the Lens suggestions // columns only when documents are done fetching so the Lens suggestions
@ -308,17 +312,18 @@ export const useDiscoverHistogram = ({
}; };
}, [isEsqlMode, stateContainer.dataState.fetchChart$, esqlFetchComplete$, unifiedHistogram]); }, [isEsqlMode, stateContainer.dataState.fetchChart$, esqlFetchComplete$, unifiedHistogram]);
const dataView = useInternalStateSelector((state) => state.dataView!); const dataView = useCurrentDataView();
const histogramCustomization = useDiscoverCustomization('unified_histogram'); const histogramCustomization = useDiscoverCustomization('unified_histogram');
const filtersMemoized = useMemo(() => { const filtersMemoized = useMemo(() => {
const allFilters = [...(filters ?? []), ...customFilters]; const allFilters = [...(filters ?? [])];
return allFilters.length ? allFilters : EMPTY_FILTERS; return allFilters.length ? allFilters : EMPTY_FILTERS;
}, [filters, customFilters]); }, [filters]);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const timeRangeMemoized = useMemo(() => timeRange, [timeRange?.from, timeRange?.to]); const timeRangeMemoized = useMemo(() => timeRange, [timeRange?.from, timeRange?.to]);
const dispatch = useInternalStateDispatch();
const onVisContextChanged = useCallback( const onVisContextChanged = useCallback(
( (
@ -332,31 +337,25 @@ export const useDiscoverHistogram = ({
stateContainer.savedSearchState.updateVisContext({ stateContainer.savedSearchState.updateVisContext({
nextVisContext, nextVisContext,
}); });
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation( dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
undefined
);
break; break;
case UnifiedHistogramExternalVisContextStatus.automaticallyOverridden: case UnifiedHistogramExternalVisContextStatus.automaticallyOverridden:
// if the visualization was invalidated as incompatible and rebuilt // if the visualization was invalidated as incompatible and rebuilt
// (it will be used later for saving the visualization via Save button) // (it will be used later for saving the visualization via Save button)
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation( dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(nextVisContext));
nextVisContext
);
break; break;
case UnifiedHistogramExternalVisContextStatus.automaticallyCreated: case UnifiedHistogramExternalVisContextStatus.automaticallyCreated:
case UnifiedHistogramExternalVisContextStatus.applied: case UnifiedHistogramExternalVisContextStatus.applied:
// clearing the value in the internal state so we don't use it during saved search saving // clearing the value in the internal state so we don't use it during saved search saving
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation( dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
undefined
);
break; break;
case UnifiedHistogramExternalVisContextStatus.unknown: case UnifiedHistogramExternalVisContextStatus.unknown:
// using `{}` to overwrite the value inside the saved search SO during saving // using `{}` to overwrite the value inside the saved search SO during saving
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation({}); dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation({}));
break; break;
} }
}, },
[stateContainer] [dispatch, stateContainer.savedSearchState]
); );
const breakdownField = useAppStateSelector((state) => state.breakdownField); const breakdownField = useAppStateSelector((state) => state.breakdownField);

View file

@ -25,7 +25,6 @@ import { DataDocuments$ } from '../../state_management/discover_data_state_conta
import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs'; import { stubLogstashDataView } from '@kbn/data-plugin/common/stubs';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { DiscoverAppStateProvider } from '../../state_management/discover_app_state_container';
import * as ExistingFieldsServiceApi from '@kbn/unified-field-list/src/services/field_existing/load_field_existing'; import * as ExistingFieldsServiceApi from '@kbn/unified-field-list/src/services/field_existing/load_field_existing';
import { resetExistingFieldsCache } from '@kbn/unified-field-list/src/hooks/use_existing_fields'; import { resetExistingFieldsCache } from '@kbn/unified-field-list/src/hooks/use_existing_fields';
import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { createDiscoverServicesMock } from '../../../../__mocks__/services';
@ -34,7 +33,8 @@ import { buildDataTableRecord } from '@kbn/discover-utils';
import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service'; import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
import { FieldListCustomization, SearchBarCustomization } from '../../../../customizations'; import { FieldListCustomization, SearchBarCustomization } from '../../../../customizations';
import { InternalStateProvider } from '../../state_management/discover_internal_state_container'; import { RuntimeStateProvider } from '../../state_management/redux';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
const mockSearchBarCustomization: SearchBarCustomization = { const mockSearchBarCustomization: SearchBarCustomization = {
id: 'search_bar', id: 'search_bar',
@ -193,7 +193,7 @@ async function mountComponent(
services?: DiscoverServices services?: DiscoverServices
): Promise<ReactWrapper<DiscoverSidebarResponsiveProps>> { ): Promise<ReactWrapper<DiscoverSidebarResponsiveProps>> {
let comp: ReactWrapper<DiscoverSidebarResponsiveProps>; let comp: ReactWrapper<DiscoverSidebarResponsiveProps>;
const { appState, internalState } = getStateContainer(appStateParams); const stateContainer = getStateContainer(appStateParams);
const mockedServices = services ?? createMockServices(); const mockedServices = services ?? createMockServices();
mockedServices.data.dataViews.getIdsWithTitle = jest.fn(async () => mockedServices.data.dataViews.getIdsWithTitle = jest.fn(async () =>
props.selectedDataView props.selectedDataView
@ -203,16 +203,18 @@ async function mountComponent(
mockedServices.data.dataViews.get = jest.fn().mockImplementation(async (id) => { mockedServices.data.dataViews.get = jest.fn().mockImplementation(async (id) => {
return [props.selectedDataView].find((d) => d!.id === id); return [props.selectedDataView].find((d) => d!.id === id);
}); });
mockedServices.data.query.getState = jest.fn().mockImplementation(() => appState.getState()); mockedServices.data.query.getState = jest
.fn()
.mockImplementation(() => stateContainer.appState.getState());
await act(async () => { await act(async () => {
comp = mountWithIntl( comp = mountWithIntl(
<KibanaContextProvider services={mockedServices}> <KibanaContextProvider services={mockedServices}>
<DiscoverAppStateProvider value={appState}> <DiscoverMainProvider value={stateContainer}>
<InternalStateProvider value={internalState}> <RuntimeStateProvider currentDataView={props.selectedDataView!} adHocDataViews={[]}>
<DiscoverSidebarResponsive {...props} /> <DiscoverSidebarResponsive {...props} />{' '}
</InternalStateProvider> </RuntimeStateProvider>
</DiscoverAppStateProvider> </DiscoverMainProvider>
</KibanaContextProvider> </KibanaContextProvider>
); );
// wait for lazy modules // wait for lazy modules

View file

@ -37,10 +37,7 @@ import {
import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverCustomization } from '../../../../customizations';
import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { import { useDataViewsForPicker } from '../../state_management/redux';
selectDataViewsForPicker,
useInternalStateSelector,
} from '../../state_management/discover_internal_state_container';
const EMPTY_FIELD_COUNTS = {}; const EMPTY_FIELD_COUNTS = {};
@ -176,8 +173,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
); );
const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView); const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView);
const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL; const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL;
const { savedDataViews, managedDataViews, adHocDataViews } = const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker();
useInternalStateSelector(selectDataViewsForPicker);
useEffect(() => { useEffect(() => {
const subscription = props.documents$.subscribe((documentState) => { const subscription = props.documents$.subscribe((documentState) => {

View file

@ -19,6 +19,7 @@ import type { SearchBarCustomization, TopNavCustomization } from '../../../../cu
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service'; import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverCustomization } from '../../../../customizations';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
jest.mock('@kbn/kibana-react-plugin/public', () => ({ jest.mock('@kbn/kibana-react-plugin/public', () => ({
...jest.requireActual('@kbn/kibana-react-plugin/public'), ...jest.requireActual('@kbn/kibana-react-plugin/public'),
@ -69,7 +70,7 @@ function getProps(
mockDiscoverService.capabilities = capabilities as typeof mockDiscoverService.capabilities; mockDiscoverService.capabilities = capabilities as typeof mockDiscoverService.capabilities;
} }
const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.transitions.setDataView(dataViewMock); stateContainer.internalState.dispatch(internalStateActions.setDataView(dataViewMock));
return { return {
stateContainer, stateContainer,
@ -110,7 +111,9 @@ describe('Discover topnav component', () => {
const props = getProps({ capabilities: { discover_v2: { save: true } } }); const props = getProps({ capabilities: { discover_v2: { save: true } } });
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );
const topNavMenu = component.find(TopNavMenu); const topNavMenu = component.find(TopNavMenu);
@ -122,7 +125,9 @@ describe('Discover topnav component', () => {
const props = getProps({ capabilities: { discover_v2: { save: false } } }); const props = getProps({ capabilities: { discover_v2: { save: false } } });
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );
const topNavMenu = component.find(TopNavMenu).props(); const topNavMenu = component.find(TopNavMenu).props();
@ -144,7 +149,9 @@ describe('Discover topnav component', () => {
const props = getProps(); const props = getProps();
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );
const topNavMenu = component.find(TopNavMenu); const topNavMenu = component.find(TopNavMenu);
@ -164,7 +171,9 @@ describe('Discover topnav component', () => {
const props = getProps(); const props = getProps();
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );
@ -176,7 +185,9 @@ describe('Discover topnav component', () => {
const props = getProps(); const props = getProps();
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );
const topNav = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu); const topNav = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu);
@ -197,7 +208,9 @@ describe('Discover topnav component', () => {
const props = getProps(); const props = getProps();
const component = mountWithIntl( const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}> <DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
); );

View file

@ -13,10 +13,6 @@ import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { DiscoverFlyouts, dismissAllFlyoutsExceptFor } from '@kbn/discover-utils'; import { DiscoverFlyouts, dismissAllFlyoutsExceptFor } from '@kbn/discover-utils';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider'; import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants'; import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants';
import {
selectDataViewsForPicker,
useInternalStateSelector,
} from '../../state_management/discover_internal_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import type { DiscoverStateContainer } from '../../state_management/discover_state'; import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { onSaveSearch } from './on_save_search'; import { onSaveSearch } from './on_save_search';
@ -26,6 +22,13 @@ import { useDiscoverTopNav } from './use_discover_topnav';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { ESQLToDataViewTransitionModal } from './esql_dataview_transition'; import { ESQLToDataViewTransitionModal } from './esql_dataview_transition';
import './top_nav.scss'; import './top_nav.scss';
import {
internalStateActions,
useCurrentDataView,
useDataViewsForPicker,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
export interface DiscoverTopNavProps { export interface DiscoverTopNavProps {
savedQuery?: string; savedQuery?: string;
@ -46,12 +49,12 @@ export const DiscoverTopNav = ({
isLoading, isLoading,
onCancelClick, onCancelClick,
}: DiscoverTopNavProps) => { }: DiscoverTopNavProps) => {
const dispatch = useInternalStateDispatch();
const services = useDiscoverServices(); const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data, setHeaderActionMenu } = services; const { dataViewEditor, navigation, dataViewFieldEditor, data, setHeaderActionMenu } = services;
const query = useAppStateSelector((state) => state.query); const query = useAppStateSelector((state) => state.query);
const { savedDataViews, managedDataViews, adHocDataViews } = const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker();
useInternalStateSelector(selectDataViewsForPicker); const dataView = useCurrentDataView();
const dataView = useInternalStateSelector((state) => state.dataView!);
const isESQLToDataViewTransitionModalVisible = useInternalStateSelector( const isESQLToDataViewTransitionModalVisible = useInternalStateSelector(
(state) => state.isESQLToDataViewTransitionModalVisible (state) => state.isESQLToDataViewTransitionModalVisible
); );
@ -134,7 +137,7 @@ export const DiscoverTopNav = ({
if (shouldDismissModal) { if (shouldDismissModal) {
services.storage.set(ESQL_TRANSITION_MODAL_KEY, true); services.storage.set(ESQL_TRANSITION_MODAL_KEY, true);
} }
stateContainer.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(false); dispatch(internalStateActions.setIsESQLToDataViewTransitionModalVisible(false));
// the user dismissed the modal, we don't need to save the search or switch to the data view mode // the user dismissed the modal, we don't need to save the search or switch to the data view mode
if (needsSave == null) { if (needsSave == null) {
return; return;
@ -145,9 +148,7 @@ export const DiscoverTopNav = ({
services, services,
state: stateContainer, state: stateContainer,
onClose: () => onClose: () =>
stateContainer.internalState.transitions.setIsESQLToDataViewTransitionModalVisible( dispatch(internalStateActions.setIsESQLToDataViewTransitionModalVisible(false)),
false
),
onSaveCb: () => { onSaveCb: () => {
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? ''); stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
}, },
@ -156,7 +157,7 @@ export const DiscoverTopNav = ({
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? ''); stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
} }
}, },
[dataView.id, services, stateContainer] [dataView.id, dispatch, services, stateContainer]
); );
const { topNavBadges, topNavMenu } = useDiscoverTopNav({ stateContainer }); const { topNavBadges, topNavMenu } = useDiscoverTopNav({ stateContainer });

View file

@ -20,6 +20,7 @@ import { discoverServiceMock } from '../../../../__mocks__/services';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
import { mockCustomizationContext } from '../../../../customizations/__mocks__/customization_context'; import { mockCustomizationContext } from '../../../../customizations/__mocks__/customization_context';
import { createRuntimeStateManager } from '../../state_management/redux';
function getStateContainer({ dataView }: { dataView?: DataView } = {}) { function getStateContainer({ dataView }: { dataView?: DataView } = {}) {
const savedSearch = savedSearchMock; const savedSearch = savedSearchMock;
@ -28,13 +29,14 @@ function getStateContainer({ dataView }: { dataView?: DataView } = {}) {
services: discoverServiceMock, services: discoverServiceMock,
history, history,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
}); });
stateContainer.savedSearchState.set(savedSearch); stateContainer.savedSearchState.set(savedSearch);
stateContainer.appState.getState = jest.fn(() => ({ stateContainer.appState.getState = jest.fn(() => ({
rowsPerPage: 250, rowsPerPage: 250,
})); }));
if (dataView) { if (dataView) {
stateContainer.internalState.transitions.setDataView(dataView); stateContainer.actions.setDataView(dataView);
} }
return stateContainer; return stateContainer;
} }

View file

@ -16,6 +16,7 @@ import { SavedSearch, SaveSavedSearchOptions } from '@kbn/saved-search-plugin/pu
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { DiscoverStateContainer } from '../../state_management/discover_state'; import { DiscoverStateContainer } from '../../state_management/discover_state';
import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size'; import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size';
import { internalStateActions } from '../../state_management/redux';
async function saveDataSource({ async function saveDataSource({
savedSearch, savedSearch,
@ -92,7 +93,7 @@ export async function onSaveSearch({
onSaveCb?: () => void; onSaveCb?: () => void;
}) { }) {
const { uiSettings, savedObjectsTagging } = services; const { uiSettings, savedObjectsTagging } = services;
const dataView = state.internalState.getState().dataView; const dataView = savedSearch.searchSource.getField('index');
const overriddenVisContextAfterInvalidation = const overriddenVisContextAfterInvalidation =
state.internalState.getState().overriddenVisContextAfterInvalidation; state.internalState.getState().overriddenVisContextAfterInvalidation;
@ -174,7 +175,7 @@ export async function onSaveSearch({
savedSearch.tags = currentTags; savedSearch.tags = currentTags;
} }
} else { } else {
state.internalState.transitions.resetOnSavedSearchChange(); state.internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
state.appState.resetInitialState(); state.appState.resetInitialState();
} }

View file

@ -13,7 +13,6 @@ import { useDiscoverCustomization } from '../../../../customizations';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { useInspector } from '../../hooks/use_inspector'; import { useInspector } from '../../hooks/use_inspector';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { import {
useSavedSearch, useSavedSearch,
useSavedSearchHasChanged, useSavedSearchHasChanged,
@ -21,6 +20,7 @@ import {
import type { DiscoverStateContainer } from '../../state_management/discover_state'; import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { getTopNavBadges } from './get_top_nav_badges'; import { getTopNavBadges } from './get_top_nav_badges';
import { useTopNavLinks } from './use_top_nav_links'; import { useTopNavLinks } from './use_top_nav_links';
import { useAdHocDataViews, useCurrentDataView } from '../../state_management/redux';
export const useDiscoverTopNav = ({ export const useDiscoverTopNav = ({
stateContainer, stateContainer,
@ -47,8 +47,8 @@ export const useDiscoverTopNav = ({
const savedSearchId = useSavedSearch().id; const savedSearchId = useSavedSearch().id;
const savedSearchHasChanged = useSavedSearchHasChanged(); const savedSearchHasChanged = useSavedSearchHasChanged();
const shouldShowESQLToDataViewTransitionModal = !savedSearchId || savedSearchHasChanged; const shouldShowESQLToDataViewTransitionModal = !savedSearchId || savedSearchHasChanged;
const dataView = useInternalStateSelector((state) => state.dataView); const dataView = useCurrentDataView();
const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews); const adHocDataViews = useAdHocDataViews();
const isEsqlMode = useIsEsqlMode(); const isEsqlMode = useIsEsqlMode();
const onOpenInspector = useInspector({ const onOpenInspector = useInspector({
inspector: services.inspector, inspector: services.inspector,

View file

@ -15,6 +15,7 @@ import { useTopNavLinks } from './use_top_nav_links';
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { createDiscoverServicesMock } from '../../../../__mocks__/services';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
describe('useTopNavLinks', () => { describe('useTopNavLinks', () => {
const services = { const services = {
@ -33,7 +34,11 @@ describe('useTopNavLinks', () => {
state.actions.setDataView(dataViewMock); state.actions.setDataView(dataViewMock);
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <KibanaContextProvider services={services}>{children}</KibanaContextProvider>; return (
<KibanaContextProvider services={services}>
<DiscoverMainProvider value={state}>{children}</DiscoverMainProvider>
</KibanaContextProvider>
);
}; };
test('useTopNavLinks result', () => { test('useTopNavLinks result', () => {

View file

@ -29,6 +29,7 @@ import {
} from './app_menu_actions'; } from './app_menu_actions';
import type { TopNavCustomization } from '../../../../customizations'; import type { TopNavCustomization } from '../../../../customizations';
import { useProfileAccessor } from '../../../../context_awareness'; import { useProfileAccessor } from '../../../../context_awareness';
import { internalStateActions, useInternalStateDispatch } from '../../state_management/redux';
/** /**
* Helper function to build the top nav links * Helper function to build the top nav links
@ -52,6 +53,7 @@ export const useTopNavLinks = ({
topNavCustomization: TopNavCustomization | undefined; topNavCustomization: TopNavCustomization | undefined;
shouldShowESQLToDataViewTransitionModal: boolean; shouldShowESQLToDataViewTransitionModal: boolean;
}): TopNavMenuData[] => { }): TopNavMenuData[] => {
const dispatch = useInternalStateDispatch();
const [newSearchUrl, setNewSearchUrl] = useState<string | undefined>(undefined); const [newSearchUrl, setNewSearchUrl] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -68,10 +70,10 @@ export const useTopNavLinks = ({
adHocDataViews, adHocDataViews,
onUpdateAdHocDataViews: async (adHocDataViewList) => { onUpdateAdHocDataViews: async (adHocDataViewList) => {
await state.actions.loadDataViewList(); await state.actions.loadDataViewList();
state.internalState.transitions.setAdHocDataViews(adHocDataViewList); dispatch(internalStateActions.setAdHocDataViews(adHocDataViewList));
}, },
}), }),
[isEsqlMode, dataView, adHocDataViews, state] [isEsqlMode, dataView, adHocDataViews, state.actions, dispatch]
); );
const defaultMenu = topNavCustomization?.defaultMenu; const defaultMenu = topNavCustomization?.defaultMenu;
@ -180,7 +182,7 @@ export const useTopNavLinks = ({
shouldShowESQLToDataViewTransitionModal && shouldShowESQLToDataViewTransitionModal &&
!services.storage.get(ESQL_TRANSITION_MODAL_KEY) !services.storage.get(ESQL_TRANSITION_MODAL_KEY)
) { ) {
state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true); dispatch(internalStateActions.setIsESQLToDataViewTransitionModalVisible(true));
} else { } else {
state.actions.transitionFromESQLToDataView(dataView.id ?? ''); state.actions.transitionFromESQLToDataView(dataView.id ?? '');
} }
@ -223,12 +225,13 @@ export const useTopNavLinks = ({
return entries; return entries;
}, [ }, [
services,
appMenuRegistry, appMenuRegistry,
state, services,
dataView, defaultMenu?.saveItem?.disabled,
isEsqlMode, isEsqlMode,
dataView,
shouldShowESQLToDataViewTransitionModal, shouldShowESQLToDataViewTransitionModal,
defaultMenu, dispatch,
state,
]); ]);
}; };

View file

@ -26,6 +26,7 @@ import { fetchEsql } from './fetch_esql';
import { buildDataTableRecord } from '@kbn/discover-utils'; import { buildDataTableRecord } from '@kbn/discover-utils';
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__'; import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { searchResponseIncompleteWarningLocalCluster } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
import { createInternalStateStore, createRuntimeStateManager } from '../state_management/redux';
jest.mock('./fetch_documents', () => ({ jest.mock('./fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue([]), fetchDocuments: jest.fn().mockResolvedValue([]),
@ -67,22 +68,9 @@ describe('test fetchAll', () => {
abortController: new AbortController(), abortController: new AbortController(),
inspectorAdapters: { requests: new RequestAdapter() }, inspectorAdapters: { requests: new RequestAdapter() },
getAppState: () => ({}), getAppState: () => ({}),
getInternalState: () => ({ internalState: createInternalStateStore({
dataView: undefined, services: discoverServiceMock,
isDataViewLoading: false, runtimeStateManager: createRuntimeStateManager(),
savedDataViews: [],
adHocDataViews: [],
defaultProfileAdHocDataViewIds: [],
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
resetId: 'test',
columns: false,
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}), }),
searchSessionId: '123', searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED, initialFetchStatus: FetchStatus.UNINITIALIZED,
@ -261,22 +249,9 @@ describe('test fetchAll', () => {
savedSearch: savedSearchMock, savedSearch: savedSearchMock,
services: discoverServiceMock, services: discoverServiceMock,
getAppState: () => ({ query }), getAppState: () => ({ query }),
getInternalState: () => ({ internalState: createInternalStateStore({
dataView: undefined, services: discoverServiceMock,
isDataViewLoading: false, runtimeStateManager: createRuntimeStateManager(),
savedDataViews: [],
adHocDataViews: [],
defaultProfileAdHocDataViewIds: [],
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
resetId: 'test',
columns: false,
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}), }),
}; };
fetchAll(subjects, false, deps); fetchAll(subjects, false, deps);
@ -386,22 +361,9 @@ describe('test fetchAll', () => {
savedSearch: savedSearchMock, savedSearch: savedSearchMock,
services: discoverServiceMock, services: discoverServiceMock,
getAppState: () => ({ query }), getAppState: () => ({ query }),
getInternalState: () => ({ internalState: createInternalStateStore({
dataView: undefined, services: discoverServiceMock,
isDataViewLoading: false, runtimeStateManager: createRuntimeStateManager(),
savedDataViews: [],
adHocDataViews: [],
defaultProfileAdHocDataViewIds: [],
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
resetId: 'test',
columns: false,
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}), }),
}; };
fetchAll(subjects, false, deps); fetchAll(subjects, false, deps);

View file

@ -42,12 +42,12 @@ import {
} from '../state_management/discover_data_state_container'; } from '../state_management/discover_data_state_container';
import { DiscoverServices } from '../../../build_services'; import { DiscoverServices } from '../../../build_services';
import { fetchEsql } from './fetch_esql'; import { fetchEsql } from './fetch_esql';
import { InternalState } from '../state_management/discover_internal_state_container'; import { InternalStateStore } from '../state_management/redux';
export interface FetchDeps { export interface FetchDeps {
abortController: AbortController; abortController: AbortController;
getAppState: () => DiscoverAppState; getAppState: () => DiscoverAppState;
getInternalState: () => InternalState; internalState: InternalStateStore;
initialFetchStatus: FetchStatus; initialFetchStatus: FetchStatus;
inspectorAdapters: Adapters; inspectorAdapters: Adapters;
savedSearch: SavedSearch; savedSearch: SavedSearch;
@ -71,7 +71,7 @@ export function fetchAll(
const { const {
initialFetchStatus, initialFetchStatus,
getAppState, getAppState,
getInternalState, internalState,
services, services,
inspectorAdapters, inspectorAdapters,
savedSearch, savedSearch,
@ -96,8 +96,7 @@ export function fetchAll(
dataView, dataView,
services, services,
sort: getAppState().sort as SortOrder[], sort: getAppState().sort as SortOrder[],
customFilters: getInternalState().customFilters, inputTimeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
inputTimeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
}); });
} }
@ -118,7 +117,7 @@ export function fetchAll(
data, data,
expressions, expressions,
profilesManager, profilesManager,
timeRange: getInternalState().dataRequestParams.timeRangeAbsolute, timeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
}) })
: fetchDocuments(searchSource, fetchDeps); : fetchDocuments(searchSource, fetchDeps);
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments'; const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
@ -221,7 +220,7 @@ export async function fetchMoreDocuments(
fetchDeps: FetchDeps fetchDeps: FetchDeps
): Promise<void> { ): Promise<void> {
try { try {
const { getAppState, getInternalState, services, savedSearch } = fetchDeps; const { getAppState, services, savedSearch } = fetchDeps;
const searchSource = savedSearch.searchSource.createChild(); const searchSource = savedSearch.searchSource.createChild();
const dataView = searchSource.getField('index')!; const dataView = searchSource.getField('index')!;
const query = getAppState().query; const query = getAppState().query;
@ -249,7 +248,6 @@ export async function fetchMoreDocuments(
dataView, dataView,
services, services,
sort: getAppState().sort as SortOrder[], sort: getAppState().sort as SortOrder[],
customFilters: getInternalState().customFilters,
}); });
// Fetch more documents // Fetch more documents

View file

@ -12,7 +12,6 @@ import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_so
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import type { SortOrder } from '@kbn/saved-search-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { discoverServiceMock } from '../../../__mocks__/services'; import { discoverServiceMock } from '../../../__mocks__/services';
import { Filter } from '@kbn/es-query';
describe('updateVolatileSearchSource', () => { describe('updateVolatileSearchSource', () => {
test('updates a given search source', async () => { test('updates a given search source', async () => {
@ -22,24 +21,9 @@ describe('updateVolatileSearchSource', () => {
dataView: dataViewMock, dataView: dataViewMock,
services: discoverServiceMock, services: discoverServiceMock,
sort: [] as SortOrder[], sort: [] as SortOrder[],
customFilters: [],
}); });
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: true }]); expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: true }]);
expect(searchSource.getField('fieldsFromSource')).toBe(undefined); expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
}); });
test('should properly update the search source with the given custom filters', async () => {
const searchSource = createSearchSourceMock({});
const filter = { meta: { index: 'foo', key: 'bar' } } as Filter;
updateVolatileSearchSource(searchSource, {
dataView: dataViewMock,
services: discoverServiceMock,
sort: [] as SortOrder[],
customFilters: [filter],
});
expect(searchSource.getField('filter')).toEqual([filter]);
});
}); });

View file

@ -9,7 +9,7 @@
import type { ISearchSource } from '@kbn/data-plugin/public'; import type { ISearchSource } from '@kbn/data-plugin/public';
import { DataViewType, type DataView } from '@kbn/data-views-plugin/public'; import { DataViewType, type DataView } from '@kbn/data-views-plugin/public';
import type { Filter, TimeRange } from '@kbn/es-query'; import type { TimeRange } from '@kbn/es-query';
import type { SortOrder } from '@kbn/saved-search-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import { DiscoverServices } from '../../../build_services'; import { DiscoverServices } from '../../../build_services';
@ -24,13 +24,11 @@ export function updateVolatileSearchSource(
dataView, dataView,
services, services,
sort, sort,
customFilters,
inputTimeRange, inputTimeRange,
}: { }: {
dataView: DataView; dataView: DataView;
services: DiscoverServices; services: DiscoverServices;
sort?: SortOrder[]; sort?: SortOrder[];
customFilters: Filter[];
inputTimeRange?: TimeRange; inputTimeRange?: TimeRange;
} }
) { ) {
@ -46,16 +44,14 @@ export function updateVolatileSearchSource(
searchSource.setField('trackTotalHits', true); searchSource.setField('trackTotalHits', true);
let filters = [...customFilters];
if (dataView.type !== DataViewType.ROLLUP) { if (dataView.type !== DataViewType.ROLLUP) {
// Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range // Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range
const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange); searchSource.setField(
filters = timeFilter ? [...filters, timeFilter] : filters; 'filter',
data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange)
);
} }
searchSource.setField('filter', filters);
searchSource.removeField('fieldsFromSource'); searchSource.removeField('fieldsFromSource');
searchSource.setField('fields', [{ field: '*', include_unmapped: true }]); searchSource.setField('fields', [{ field: '*', include_unmapped: true }]);
} }

View file

@ -20,6 +20,7 @@ import { Router } from '@kbn/shared-ux-router';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
import { DiscoverMainProvider } from './state_management/discover_state_provider'; import { DiscoverMainProvider } from './state_management/discover_state_provider';
import { RuntimeStateProvider, internalStateActions } from './state_management/redux';
discoverServiceMock.data.query.timefilter.timefilter.getTime = () => { discoverServiceMock.data.query.timefilter.timefilter.getTime = () => {
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
@ -32,7 +33,7 @@ describe('DiscoverMainApp', () => {
}) as unknown as DataViewListItem[]; }) as unknown as DataViewListItem[];
const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.actions.setDataView(dataViewMock); stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setSavedDataViews(dataViewList); stateContainer.internalState.dispatch(internalStateActions.setSavedDataViews(dataViewList));
const props = { const props = {
stateContainer, stateContainer,
}; };
@ -41,11 +42,13 @@ describe('DiscoverMainApp', () => {
}); });
await act(async () => { await act(async () => {
const component = await mountWithIntl( const component = mountWithIntl(
<Router history={history}> <Router history={history}>
<KibanaContextProvider services={discoverServiceMock}> <KibanaContextProvider services={discoverServiceMock}>
<DiscoverMainProvider value={stateContainer}> <DiscoverMainProvider value={stateContainer}>
<DiscoverMainApp {...props} /> <RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverMainApp {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
</KibanaContextProvider> </KibanaContextProvider>
</Router> </Router>
@ -53,7 +56,7 @@ describe('DiscoverMainApp', () => {
// wait for lazy modules // wait for lazy modules
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
await component.update(); component.update();
expect(component.find(DiscoverTopNav).exists()).toBe(true); expect(component.find(DiscoverTopNav).exists()).toBe(true);
}); });

View file

@ -31,10 +31,7 @@ jest.mock('../../customizations', () => {
const originalModule = jest.requireActual('../../customizations'); const originalModule = jest.requireActual('../../customizations');
return { return {
...originalModule, ...originalModule,
useDiscoverCustomizationService: () => ({ useDiscoverCustomizationService: () => mockCustomizationService,
customizationService: mockCustomizationService,
isInitialized: Boolean(mockCustomizationService),
}),
}; };
}); });

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import React, { useEffect, useState, memo, useCallback, useMemo } from 'react'; import React, { useEffect, useState, memo, useCallback, useMemo, lazy, ReactNode } from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import { import {
@ -22,6 +22,10 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { withSuspense } from '@kbn/shared-ux-utility'; import { withSuspense } from '@kbn/shared-ux-utility';
import { getInitialESQLQuery } from '@kbn/esql-utils'; import { getInitialESQLQuery } from '@kbn/esql-utils';
import { ESQL_TYPE } from '@kbn/data-view-utils'; import { ESQL_TYPE } from '@kbn/data-view-utils';
import type {
AnalyticsNoDataPageKibanaDependencies,
AnalyticsNoDataPageProps,
} from '@kbn/shared-ux-page-analytics-no-data-types';
import { useUrl } from './hooks/use_url'; import { useUrl } from './hooks/use_url';
import { useDiscoverStateContainer } from './hooks/use_discover_state_container'; import { useDiscoverStateContainer } from './hooks/use_discover_state_container';
import { MainHistoryLocationState } from '../../../common'; import { MainHistoryLocationState } from '../../../common';
@ -41,6 +45,12 @@ import {
import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state';
import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources';
import { useDefaultAdHocDataViews, useRootProfile } from '../../context_awareness'; import { useDefaultAdHocDataViews, useRootProfile } from '../../context_awareness';
import {
RuntimeStateManager,
RuntimeStateProvider,
createRuntimeStateManager,
useRuntimeState,
} from './state_management/redux';
const DiscoverMainAppMemoized = memo(DiscoverMainApp); const DiscoverMainAppMemoized = memo(DiscoverMainApp);
@ -72,17 +82,18 @@ export function DiscoverMainRoute({
getScopedHistory, getScopedHistory,
} = services; } = services;
const { id: savedSearchId } = useParams<DiscoverLandingParams>(); const { id: savedSearchId } = useParams<DiscoverLandingParams>();
const [runtimeStateManager] = useState(() => createRuntimeStateManager());
const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({ const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({
history, history,
services, services,
customizationContext, customizationContext,
stateStorageContainer, stateStorageContainer,
runtimeStateManager,
});
const customizationService = useDiscoverCustomizationService({
customizationCallbacks,
stateContainer,
}); });
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
useDiscoverCustomizationService({
customizationCallbacks,
stateContainer,
});
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [noDataState, setNoDataState] = useState({ const [noDataState, setNoDataState] = useState({
@ -110,6 +121,7 @@ export function DiscoverMainRoute({
page: 'app', page: 'app',
id: savedSearchId || 'new', id: savedSearchId || 'new',
}); });
/** /**
* Helper function to determine when to skip the no data page * Helper function to determine when to skip the no data page
*/ */
@ -134,8 +146,7 @@ export function DiscoverMainRoute({
]); ]);
const persistedDataViewsExist = hasUserDataViewValue && defaultDataViewExists; const persistedDataViewsExist = hasUserDataViewValue && defaultDataViewExists;
const adHocDataViewsExist = const adHocDataViewsExist = runtimeStateManager.adHocDataViews$.getValue().length > 0;
stateContainer.internalState.getState().adHocDataViews.length > 0;
const locationStateHasDataViewSpec = Boolean(historyLocationState?.dataViewSpec); const locationStateHasDataViewSpec = Boolean(historyLocationState?.dataViewSpec);
const canAccessWithAdHocDataViews = const canAccessWithAdHocDataViews =
hasESDataValue && (adHocDataViewsExist || locationStateHasDataViewSpec); hasESDataValue && (adHocDataViewsExist || locationStateHasDataViewSpec);
@ -156,7 +167,13 @@ export function DiscoverMainRoute({
return false; return false;
} }
}, },
[data.dataViews, historyLocationState?.dataViewSpec, savedSearchId, stateContainer] [
data.dataViews,
historyLocationState?.dataViewSpec,
runtimeStateManager,
savedSearchId,
stateContainer,
]
); );
const loadSavedSearch = useCallback( const loadSavedSearch = useCallback(
@ -243,7 +260,7 @@ export function DiscoverMainRoute({
}); });
useEffect(() => { useEffect(() => {
if (!isCustomizationServiceInitialized || rootProfileState.rootProfileLoading) { if (!customizationService || rootProfileState.rootProfileLoading) {
return; return;
} }
@ -262,16 +279,17 @@ export function DiscoverMainRoute({
await loadSavedSearch(); await loadSavedSearch();
} else { } else {
// restore the previously selected data view for a new state (when a saved search was open) // restore the previously selected data view for a new state (when a saved search was open)
await loadSavedSearch(getLoadParamsForNewSearch(stateContainer)); await loadSavedSearch(getLoadParamsForNewSearch({ stateContainer, runtimeStateManager }));
} }
}; };
load(); load();
}, [ }, [
customizationService,
initializeProfileDataViews, initializeProfileDataViews,
isCustomizationServiceInitialized,
loadSavedSearch, loadSavedSearch,
rootProfileState.rootProfileLoading, rootProfileState.rootProfileLoading,
runtimeStateManager,
savedSearchId, savedSearchId,
stateContainer, stateContainer,
]); ]);
@ -280,10 +298,10 @@ export function DiscoverMainRoute({
useUrl({ useUrl({
history, history,
savedSearchId, savedSearchId,
onNewUrl: () => { onNewUrl: useCallback(() => {
// restore the previously selected data view for a new state // restore the previously selected data view for a new state
loadSavedSearch(getLoadParamsForNewSearch(stateContainer)); loadSavedSearch(getLoadParamsForNewSearch({ stateContainer, runtimeStateManager }));
}, }, [loadSavedSearch, runtimeStateManager, stateContainer]),
}); });
const onDataViewCreated = useCallback( const onDataViewCreated = useCallback(
@ -302,7 +320,7 @@ export function DiscoverMainRoute({
resetStateContainer(); resetStateContainer();
}, [resetStateContainer]); }, [resetStateContainer]);
const noDataDependencies = useMemo( const noDataDependencies = useMemo<AnalyticsNoDataPageKibanaDependencies>(
() => ({ () => ({
coreStart: core, coreStart: core,
dataViews: { dataViews: {
@ -323,62 +341,39 @@ export function DiscoverMainRoute({
[core, data.dataViews, dataViewEditor, noDataState, services.noDataPage, share] [core, data.dataViews, dataViewEditor, noDataState, services.noDataPage, share]
); );
const loadingIndicator = useMemo( const currentDataView = useRuntimeState(runtimeStateManager.currentDataView$);
() => <LoadingIndicator type={hasCustomBranding ? 'spinner' : 'elastic'} />, const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
[hasCustomBranding]
);
const mainContent = useMemo(() => {
if (noDataState.showNoDataPage) {
const importPromise = import('@kbn/shared-ux-page-analytics-no-data');
const AnalyticsNoDataPageKibanaProvider = withSuspense(
React.lazy(() =>
importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => {
return { default: NoDataProvider };
})
)
);
const AnalyticsNoDataPage = withSuspense(
React.lazy(() =>
importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => {
return { default: NoDataPage };
})
)
);
return (
<AnalyticsNoDataPageKibanaProvider {...noDataDependencies}>
<AnalyticsNoDataPage
onDataViewCreated={onDataViewCreated}
onESQLNavigationComplete={onESQLNavigationComplete}
/>
</AnalyticsNoDataPageKibanaProvider>
);
}
if (loading) {
return loadingIndicator;
}
return <DiscoverMainAppMemoized stateContainer={stateContainer} />;
}, [
loading,
loadingIndicator,
noDataDependencies,
onDataViewCreated,
onESQLNavigationComplete,
noDataState.showNoDataPage,
stateContainer,
]);
if (error) { if (error) {
return <DiscoverError error={error} />; return <DiscoverError error={error} />;
} }
const loadingIndicator = <LoadingIndicator type={hasCustomBranding ? 'spinner' : 'elastic'} />;
if (!customizationService || rootProfileState.rootProfileLoading) { if (!customizationService || rootProfileState.rootProfileLoading) {
return loadingIndicator; return loadingIndicator;
} }
let mainContent: ReactNode;
if (!loading && noDataState.showNoDataPage) {
mainContent = (
<NoDataPage
onDataViewCreated={onDataViewCreated}
onESQLNavigationComplete={onESQLNavigationComplete}
{...noDataDependencies}
/>
);
} else if (!loading && currentDataView) {
mainContent = (
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
<DiscoverMainAppMemoized stateContainer={stateContainer} />
</RuntimeStateProvider>
);
} else {
mainContent = loadingIndicator;
}
return ( return (
<DiscoverCustomizationProvider value={customizationService}> <DiscoverCustomizationProvider value={customizationService}>
<DiscoverMainProvider value={stateContainer}> <DiscoverMainProvider value={stateContainer}>
@ -387,15 +382,45 @@ export function DiscoverMainRoute({
</DiscoverCustomizationProvider> </DiscoverCustomizationProvider>
); );
} }
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default DiscoverMainRoute; export default DiscoverMainRoute;
function getLoadParamsForNewSearch(stateContainer: DiscoverStateContainer): { const NoDataPage = ({
onDataViewCreated,
onESQLNavigationComplete,
...noDataDependencies
}: AnalyticsNoDataPageKibanaDependencies & AnalyticsNoDataPageProps) => {
const importPromise = import('@kbn/shared-ux-page-analytics-no-data');
const AnalyticsNoDataPageKibanaProvider = withSuspense(
lazy(async () => ({ default: (await importPromise).AnalyticsNoDataPageKibanaProvider }))
);
const AnalyticsNoDataPage = withSuspense(
lazy(async () => ({ default: (await importPromise).AnalyticsNoDataPage }))
);
return (
<AnalyticsNoDataPageKibanaProvider {...noDataDependencies}>
<AnalyticsNoDataPage
onDataViewCreated={onDataViewCreated}
onESQLNavigationComplete={onESQLNavigationComplete}
/>
</AnalyticsNoDataPageKibanaProvider>
);
};
function getLoadParamsForNewSearch({
stateContainer,
runtimeStateManager,
}: {
stateContainer: DiscoverStateContainer;
runtimeStateManager: RuntimeStateManager;
}): {
nextDataView: LoadParams['dataView']; nextDataView: LoadParams['dataView'];
initialAppState: LoadParams['initialAppState']; initialAppState: LoadParams['initialAppState'];
} { } {
const prevAppState = stateContainer.appState.getState(); const prevAppState = stateContainer.appState.getState();
const prevDataView = stateContainer.internalState.getState().dataView; const prevDataView = runtimeStateManager.currentDataView$.getValue();
const initialAppState = const initialAppState =
isDataSourceType(prevAppState.dataSource, DataSourceType.Esql) && isDataSourceType(prevAppState.dataSource, DataSourceType.Esql) &&
prevDataView && prevDataView &&

View file

@ -11,11 +11,11 @@ import { useEffect } from 'react';
import { METRIC_TYPE } from '@kbn/analytics'; import { METRIC_TYPE } from '@kbn/analytics';
import { DiscoverServices } from '../../../build_services'; import { DiscoverServices } from '../../../build_services';
import { useSavedSearch } from '../state_management/discover_state_provider'; import { useSavedSearch } from '../state_management/discover_state_provider';
import { useInternalStateSelector } from '../state_management/discover_internal_state_container';
import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../../../constants'; import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../../../constants';
import { DiscoverStateContainer } from '../state_management/discover_state'; import { DiscoverStateContainer } from '../state_management/discover_state';
import { useFiltersValidation } from './use_filters_validation'; import { useFiltersValidation } from './use_filters_validation';
import { useIsEsqlMode } from './use_is_esql_mode'; import { useIsEsqlMode } from './use_is_esql_mode';
import { useCurrentDataView } from '../state_management/redux';
export const useAdHocDataViews = ({ export const useAdHocDataViews = ({
services, services,
@ -23,7 +23,7 @@ export const useAdHocDataViews = ({
stateContainer: DiscoverStateContainer; stateContainer: DiscoverStateContainer;
services: DiscoverServices; services: DiscoverServices;
}) => { }) => {
const dataView = useInternalStateSelector((state) => state.dataView); const dataView = useCurrentDataView();
const savedSearch = useSavedSearch(); const savedSearch = useSavedSearch();
const isEsqlMode = useIsEsqlMode(); const isEsqlMode = useIsEsqlMode();
const { filterManager, toastNotifications } = services; const { filterManager, toastNotifications } = services;

View file

@ -26,6 +26,7 @@ import { VIEW_MODE } from '@kbn/saved-search-plugin/public';
import { dataViewAdHoc } from '../../../__mocks__/data_view_complex'; import { dataViewAdHoc } from '../../../__mocks__/data_view_complex';
import { buildDataTableRecord, EsHitRecord } from '@kbn/discover-utils'; import { buildDataTableRecord, EsHitRecord } from '@kbn/discover-utils';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { internalStateActions } from '../state_management/redux';
function getHookProps( function getHookProps(
query: AggregateQuery | Query | undefined, query: AggregateQuery | Query | undefined,
@ -37,7 +38,9 @@ function getHookProps(
const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.appState.replaceUrlState = replaceUrlState; stateContainer.appState.replaceUrlState = replaceUrlState;
stateContainer.appState.update({ columns: [], ...appState }); stateContainer.appState.update({ columns: [], ...appState });
stateContainer.internalState.transitions.setSavedDataViews([dataViewMock as DataViewListItem]); stateContainer.internalState.dispatch(
internalStateActions.setSavedDataViews([dataViewMock as DataViewListItem])
);
const msgLoading = { const msgLoading = {
fetchStatus: defaultFetchStatus, fetchStatus: defaultFetchStatus,
@ -502,7 +505,9 @@ describe('useEsqlMode', () => {
FetchStatus.LOADING FetchStatus.LOADING
); );
const documents$ = stateContainer.dataState.data$.documents$; const documents$ = stateContainer.dataState.data$.documents$;
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
@ -517,7 +522,9 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern1' }, query: { esql: 'from pattern1' },
}); });
await waitFor(() => await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true, columns: true,
rowHeight: true, rowHeight: true,
breakdownField: true, breakdownField: true,
@ -527,18 +534,22 @@ describe('useEsqlMode', () => {
fetchStatus: FetchStatus.PARTIAL, fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern1' }, query: { esql: 'from pattern1' },
}); });
stateContainer.internalState.transitions.setResetDefaultProfileState({ stateContainer.internalState.dispatch(
columns: false, internalStateActions.setResetDefaultProfileState({
rowHeight: false, columns: false,
breakdownField: false, rowHeight: false,
}); breakdownField: false,
})
);
stateContainer.appState.update({ query: { esql: 'from pattern1' } }); stateContainer.appState.update({ query: { esql: 'from pattern1' } });
documents$.next({ documents$.next({
fetchStatus: FetchStatus.LOADING, fetchStatus: FetchStatus.LOADING,
query: { esql: 'from pattern1' }, query: { esql: 'from pattern1' },
}); });
await waitFor(() => await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
@ -554,7 +565,9 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern2' }, query: { esql: 'from pattern2' },
}); });
await waitFor(() => await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true, columns: true,
rowHeight: true, rowHeight: true,
breakdownField: true, breakdownField: true,
@ -571,7 +584,9 @@ describe('useEsqlMode', () => {
const documents$ = stateContainer.dataState.data$.documents$; const documents$ = stateContainer.dataState.data$.documents$;
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)]; const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)]; const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
@ -582,7 +597,9 @@ describe('useEsqlMode', () => {
result: result1, result: result1,
}); });
await waitFor(() => await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
@ -594,7 +611,9 @@ describe('useEsqlMode', () => {
result: result2, result: result2,
}); });
await waitFor(() => await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true, columns: true,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,

View file

@ -17,6 +17,7 @@ import { useSavedSearchInitial } from '../state_management/discover_state_provid
import type { DiscoverStateContainer } from '../state_management/discover_state'; import type { DiscoverStateContainer } from '../state_management/discover_state';
import { getValidViewMode } from '../utils/get_valid_view_mode'; import { getValidViewMode } from '../utils/get_valid_view_mode';
import { FetchStatus } from '../../types'; import { FetchStatus } from '../../types';
import { internalStateActions, useInternalStateDispatch } from '../state_management/redux';
const MAX_NUM_OF_COLUMNS = 50; const MAX_NUM_OF_COLUMNS = 50;
@ -31,6 +32,7 @@ export function useEsqlMode({
stateContainer: DiscoverStateContainer; stateContainer: DiscoverStateContainer;
dataViews: DataViewsContract; dataViews: DataViewsContract;
}) { }) {
const dispatch = useInternalStateDispatch();
const savedSearch = useSavedSearchInitial(); const savedSearch = useSavedSearchInitial();
const prev = useRef<{ const prev = useRef<{
initialFetch: boolean; initialFetch: boolean;
@ -93,11 +95,13 @@ export function useEsqlMode({
// Reset all default profile state when index pattern changes // Reset all default profile state when index pattern changes
if (indexPatternChanged) { if (indexPatternChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({ dispatch(
columns: true, internalStateActions.setResetDefaultProfileState({
rowHeight: true, columns: true,
breakdownField: true, rowHeight: true,
}); breakdownField: true,
})
);
} }
} }
@ -149,11 +153,13 @@ export function useEsqlMode({
// If the index pattern hasn't changed, but the available columns have changed // If the index pattern hasn't changed, but the available columns have changed
// due to transformational commands, reset the associated default profile state // due to transformational commands, reset the associated default profile state
if (!indexPatternChanged && allColumnsChanged) { if (!indexPatternChanged && allColumnsChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({ dispatch(
columns: true, internalStateActions.setResetDefaultProfileState({
rowHeight: false, columns: true,
breakdownField: false, rowHeight: false,
}); breakdownField: false,
})
);
} }
prev.current.allColumns = nextAllColumns; prev.current.allColumns = nextAllColumns;
@ -186,5 +192,5 @@ export function useEsqlMode({
cleanup(); cleanup();
subscription.unsubscribe(); subscription.unsubscribe();
}; };
}, [dataViews, stateContainer, savedSearch, cleanup]); }, [dataViews, stateContainer, savedSearch, cleanup, dispatch]);
} }

View file

@ -15,6 +15,9 @@ import { OverlayRef } from '@kbn/core/public';
import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter';
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DataTableRecord } from '@kbn/discover-utils/types';
import { internalStateActions } from '../state_management/redux';
import React from 'react';
import { DiscoverMainProvider } from '../state_management/discover_state_provider';
describe('test useInspector', () => { describe('test useInspector', () => {
test('inspector open function is executed, expanded doc is closed', async () => { test('inspector open function is executed, expanded doc is closed', async () => {
@ -26,13 +29,22 @@ describe('test useInspector', () => {
const requests = new RequestAdapter(); const requests = new RequestAdapter();
const lensRequests = new RequestAdapter(); const lensRequests = new RequestAdapter();
const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.transitions.setExpandedDoc({} as unknown as DataTableRecord); stateContainer.internalState.dispatch(
const { result } = renderHook(() => { internalStateActions.setExpandedDoc({} as unknown as DataTableRecord)
return useInspector({ );
stateContainer, const { result } = renderHook(
inspector: discoverServiceMock.inspector, () => {
}); return useInspector({
}); stateContainer,
inspector: discoverServiceMock.inspector,
});
},
{
wrapper: ({ children }) => (
<DiscoverMainProvider value={stateContainer}>{children}</DiscoverMainProvider>
),
}
);
await act(async () => { await act(async () => {
result.current(); result.current();
}); });

View file

@ -15,6 +15,7 @@ import {
} from '@kbn/inspector-plugin/public'; } from '@kbn/inspector-plugin/public';
import { DiscoverStateContainer } from '../state_management/discover_state'; import { DiscoverStateContainer } from '../state_management/discover_state';
import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter';
import { internalStateActions, useInternalStateDispatch } from '../state_management/redux';
export interface InspectorAdapters { export interface InspectorAdapters {
requests: RequestAdapter; requests: RequestAdapter;
@ -28,11 +29,13 @@ export function useInspector({
inspector: InspectorPublicPluginStart; inspector: InspectorPublicPluginStart;
stateContainer: DiscoverStateContainer; stateContainer: DiscoverStateContainer;
}) { }) {
const dispatch = useInternalStateDispatch();
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined); const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
const onOpenInspector = useCallback(() => { const onOpenInspector = useCallback(() => {
// prevent overlapping // prevent overlapping
stateContainer.internalState.transitions.setExpandedDoc(undefined); dispatch(internalStateActions.setExpandedDoc(undefined));
const inspectorAdapters = stateContainer.dataState.inspectorAdapters; const inspectorAdapters = stateContainer.dataState.inspectorAdapters;
const requestAdapters = inspectorAdapters.lensRequests const requestAdapters = inspectorAdapters.lensRequests
@ -45,7 +48,12 @@ export function useInspector({
); );
setInspectorSession(session); setInspectorSession(session);
}, [stateContainer, inspector]); }, [
dispatch,
stateContainer.dataState.inspectorAdapters,
stateContainer.savedSearchState,
inspector,
]);
useEffect(() => { useEffect(() => {
return () => { return () => {

View file

@ -17,6 +17,7 @@ import { DiscoverServices } from '../../../build_services';
import { DiscoverStateContainer } from '../state_management/discover_state'; import { DiscoverStateContainer } from '../state_management/discover_state';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { createSavedSearchAdHocMock, createSavedSearchMock } from '../../../__mocks__/saved_search'; import { createSavedSearchAdHocMock, createSavedSearchMock } from '../../../__mocks__/saved_search';
import { internalStateActions } from '../state_management/redux';
const renderUrlTracking = ({ const renderUrlTracking = ({
services, services,
@ -55,9 +56,11 @@ describe('useUrlTracking', () => {
const services = createDiscoverServicesMock(); const services = createDiscoverServicesMock();
const savedSearch = omit(createSavedSearchAdHocMock(), 'id'); const savedSearch = omit(createSavedSearchAdHocMock(), 'id');
const stateContainer = getDiscoverStateMock({ savedSearch }); const stateContainer = getDiscoverStateMock({ savedSearch });
stateContainer.internalState.transitions.setDefaultProfileAdHocDataViews([ stateContainer.internalState.dispatch(
savedSearch.searchSource.getField('index')!, internalStateActions.setDefaultProfileAdHocDataViews([
]); savedSearch.searchSource.getField('index')!,
])
);
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled(); expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
renderUrlTracking({ services, stateContainer }); renderUrlTracking({ services, stateContainer });
expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true); expect(services.urlTracker.setTrackingEnabled).toHaveBeenCalledWith(true);

View file

@ -31,7 +31,7 @@ export function useUrlTracking(stateContainer: DiscoverStateContainer) {
// Disable for ad hoc data views, since they can't be restored after a page refresh // Disable for ad hoc data views, since they can't be restored after a page refresh
dataView.isPersisted() || dataView.isPersisted() ||
// Unless it's a default profile data view, which can be restored on refresh // Unless it's a default profile data view, which can be restored on refresh
internalState.get().defaultProfileAdHocDataViewIds.includes(dataView.id) || internalState.getState().defaultProfileAdHocDataViewIds.includes(dataView.id) ||
// Or we're in ES|QL mode, in which case we don't care about the data view // Or we're in ES|QL mode, in which case we don't care about the data view
isOfAggregateQueryType(savedSearch.searchSource.getField('query')); isOfAggregateQueryType(savedSearch.searchSource.getField('query'));

View file

@ -20,17 +20,17 @@ import { discoverServiceMock } from '../../../__mocks__/services';
import { getDiscoverAppStateContainer, isEqualState } from './discover_app_state_container'; import { getDiscoverAppStateContainer, isEqualState } from './discover_app_state_container';
import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common';
import { createDataViewDataSource } from '../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../common/data_sources';
import { getInternalStateContainer } from './discover_internal_state_container';
import { import {
DiscoverSavedSearchContainer, DiscoverSavedSearchContainer,
getSavedSearchContainer, getSavedSearchContainer,
} from './discover_saved_search_container'; } from './discover_saved_search_container';
import { getDiscoverGlobalStateContainer } from './discover_global_state_container'; import { getDiscoverGlobalStateContainer } from './discover_global_state_container';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { createInternalStateStore, createRuntimeStateManager, InternalStateStore } from './redux';
let history: History; let history: History;
let stateStorage: IKbnUrlStateStorage; let stateStorage: IKbnUrlStateStorage;
let internalState: ReturnType<typeof getInternalStateContainer>; let internalState: InternalStateStore;
let savedSearchState: DiscoverSavedSearchContainer; let savedSearchState: DiscoverSavedSearchContainer;
describe('Test discover app state container', () => { describe('Test discover app state container', () => {
@ -42,18 +42,21 @@ describe('Test discover app state container', () => {
history, history,
...(toasts && withNotifyOnErrors(toasts)), ...(toasts && withNotifyOnErrors(toasts)),
}); });
internalState = getInternalStateContainer(); internalState = createInternalStateStore({
services: discoverServiceMock,
runtimeStateManager: createRuntimeStateManager(),
});
savedSearchState = getSavedSearchContainer({ savedSearchState = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer: getDiscoverGlobalStateContainer(stateStorage), globalStateContainer: getDiscoverGlobalStateContainer(stateStorage),
internalStateContainer: internalState, internalState,
}); });
}); });
const getStateContainer = () => const getStateContainer = () =>
getDiscoverAppStateContainer({ getDiscoverAppStateContainer({
stateStorage, stateStorage,
internalStateContainer: internalState, internalState,
savedSearchContainer: savedSearchState, savedSearchContainer: savedSearchState,
services: discoverServiceMock, services: discoverServiceMock,
}); });
@ -271,13 +274,13 @@ describe('Test discover app state container', () => {
describe('initAndSync', () => { describe('initAndSync', () => {
it('should call setResetDefaultProfileState correctly with no initial state', () => { it('should call setResetDefaultProfileState correctly with no initial state', () => {
const state = getStateContainer(); const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
}); });
state.initAndSync(); state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: true, columns: true,
rowHeight: true, rowHeight: true,
breakdownField: true, breakdownField: true,
@ -288,13 +291,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get'); const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ columns: ['test'] }); stateStorageGetSpy.mockReturnValue({ columns: ['test'] });
const state = getStateContainer(); const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
}); });
state.initAndSync(); state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: true, rowHeight: true,
breakdownField: true, breakdownField: true,
@ -305,13 +308,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get'); const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ rowHeight: 5 }); stateStorageGetSpy.mockReturnValue({ rowHeight: 5 });
const state = getStateContainer(); const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
}); });
state.initAndSync(); state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: true, columns: true,
rowHeight: false, rowHeight: false,
breakdownField: true, breakdownField: true,
@ -328,13 +331,13 @@ describe('Test discover app state container', () => {
managed: false, managed: false,
}); });
const state = getStateContainer(); const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
}); });
state.initAndSync(); state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,

View file

@ -40,8 +40,8 @@ import {
DiscoverDataSource, DiscoverDataSource,
isDataSourceType, isDataSourceType,
} from '../../../../common/data_sources'; } from '../../../../common/data_sources';
import type { DiscoverInternalStateContainer } from './discover_internal_state_container';
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container'; import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
import { internalStateActions, InternalStateStore } from './redux';
export const APP_STATE_URL_KEY = '_a'; export const APP_STATE_URL_KEY = '_a';
export interface DiscoverAppStateContainer extends ReduxLikeStateContainer<DiscoverAppState> { export interface DiscoverAppStateContainer extends ReduxLikeStateContainer<DiscoverAppState> {
@ -185,12 +185,12 @@ export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelec
*/ */
export const getDiscoverAppStateContainer = ({ export const getDiscoverAppStateContainer = ({
stateStorage, stateStorage,
internalStateContainer, internalState,
savedSearchContainer, savedSearchContainer,
services, services,
}: { }: {
stateStorage: IKbnUrlStateStorage; stateStorage: IKbnUrlStateStorage;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
savedSearchContainer: DiscoverSavedSearchContainer; savedSearchContainer: DiscoverSavedSearchContainer;
services: DiscoverServices; services: DiscoverServices;
}): DiscoverAppStateContainer => { }): DiscoverAppStateContainer => {
@ -268,11 +268,13 @@ export const getDiscoverAppStateContainer = ({
const { breakdownField, columns, rowHeight } = getCurrentUrlState(stateStorage, services); const { breakdownField, columns, rowHeight } = getCurrentUrlState(stateStorage, services);
// Only set default state which is not already set in the URL // Only set default state which is not already set in the URL
internalStateContainer.transitions.setResetDefaultProfileState({ internalState.dispatch(
columns: columns === undefined, internalStateActions.setResetDefaultProfileState({
rowHeight: rowHeight === undefined, columns: columns === undefined,
breakdownField: breakdownField === undefined, rowHeight: rowHeight === undefined,
}); breakdownField: breakdownField === undefined,
})
);
} }
const { data } = services; const { data } = services;

View file

@ -17,6 +17,7 @@ import { DataDocuments$ } from './discover_data_state_container';
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
import { fetchDocuments } from '../data_fetching/fetch_documents'; import { fetchDocuments } from '../data_fetching/fetch_documents';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { internalStateActions } from './redux';
jest.mock('../data_fetching/fetch_documents', () => ({ jest.mock('../data_fetching/fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue({ records: [] }), fetchDocuments: jest.fn().mockResolvedValue({ records: [] }),
@ -176,11 +177,13 @@ describe('test getDataStateContainer', () => {
const appUnsub = stateContainer.appState.initAndSync(); const appUnsub = stateContainer.appState.initAndSync();
await discoverServiceMock.profilesManager.resolveDataSourceProfile({}); await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock); stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({ stateContainer.internalState.dispatch(
columns: true, internalStateActions.setResetDefaultProfileState({
rowHeight: true, columns: true,
breakdownField: true, rowHeight: true,
}); breakdownField: true,
})
);
dataState.data$.totalHits$.next({ dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE, fetchStatus: FetchStatus.COMPLETE,
@ -191,7 +194,9 @@ describe('test getDataStateContainer', () => {
await waitFor(() => { await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE); expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
}); });
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,
@ -209,11 +214,13 @@ describe('test getDataStateContainer', () => {
const appUnsub = stateContainer.appState.initAndSync(); const appUnsub = stateContainer.appState.initAndSync();
await discoverServiceMock.profilesManager.resolveDataSourceProfile({}); await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock); stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({ stateContainer.internalState.dispatch(
columns: false, internalStateActions.setResetDefaultProfileState({
rowHeight: false, columns: false,
breakdownField: false, rowHeight: false,
}); breakdownField: false,
})
);
dataState.data$.totalHits$.next({ dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE, fetchStatus: FetchStatus.COMPLETE,
result: 0, result: 0,
@ -222,7 +229,9 @@ describe('test getDataStateContainer', () => {
await waitFor(() => { await waitFor(() => {
expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE); expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE);
}); });
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({ expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false, columns: false,
rowHeight: false, rowHeight: false,
breakdownField: false, breakdownField: false,

View file

@ -28,8 +28,8 @@ import { validateTimeRange } from './utils/validate_time_range';
import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all';
import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { sendResetMsg } from '../hooks/use_saved_search_messages';
import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { getFetch$ } from '../data_fetching/get_fetch_observable';
import type { DiscoverInternalStateContainer } from './discover_internal_state_container';
import { getDefaultProfileState } from './utils/get_default_profile_state'; import { getDefaultProfileState } from './utils/get_default_profile_state';
import { internalStateActions, InternalStateStore, RuntimeStateManager } from './redux';
export interface SavedSearchData { export interface SavedSearchData {
main$: DataMain$; main$: DataMain$;
@ -138,14 +138,16 @@ export function getDataStateContainer({
services, services,
searchSessionManager, searchSessionManager,
appStateContainer, appStateContainer,
internalStateContainer, internalState,
runtimeStateManager,
getSavedSearch, getSavedSearch,
setDataView, setDataView,
}: { }: {
services: DiscoverServices; services: DiscoverServices;
searchSessionManager: DiscoverSearchSessionManager; searchSessionManager: DiscoverSearchSessionManager;
appStateContainer: DiscoverAppStateContainer; appStateContainer: DiscoverAppStateContainer;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
getSavedSearch: () => SavedSearch; getSavedSearch: () => SavedSearch;
setDataView: (dataView: DataView) => void; setDataView: (dataView: DataView) => void;
}): DiscoverDataStateContainer { }): DiscoverDataStateContainer {
@ -229,7 +231,7 @@ export function getDataStateContainer({
searchSessionId, searchSessionId,
services, services,
getAppState: appStateContainer.getState, getAppState: appStateContainer.getState,
getInternalState: internalStateContainer.getState, internalState,
savedSearch: getSavedSearch(), savedSearch: getSavedSearch(),
}; };
@ -254,10 +256,12 @@ export function getDataStateContainer({
return; return;
} }
internalStateContainer.transitions.setDataRequestParams({ internalState.dispatch(
timeRangeAbsolute: timefilter.getAbsoluteTime(), internalStateActions.setDataRequestParams({
timeRangeRelative: timefilter.getTime(), timeRangeAbsolute: timefilter.getAbsoluteTime(),
}); timeRangeRelative: timefilter.getTime(),
})
);
await profilesManager.resolveDataSourceProfile({ await profilesManager.resolveDataSourceProfile({
dataSource: appStateContainer.getState().dataSource, dataSource: appStateContainer.getState().dataSource,
@ -265,7 +269,8 @@ export function getDataStateContainer({
query: appStateContainer.getState().query, query: appStateContainer.getState().query,
}); });
const { resetDefaultProfileState, dataView } = internalStateContainer.getState(); const { resetDefaultProfileState } = internalState.getState();
const dataView = runtimeStateManager.currentDataView$.getValue();
const defaultProfileState = dataView const defaultProfileState = dataView
? getDefaultProfileState({ profilesManager, resetDefaultProfileState, dataView }) ? getDefaultProfileState({ profilesManager, resetDefaultProfileState, dataView })
: undefined; : undefined;
@ -294,7 +299,7 @@ export function getDataStateContainer({
}, },
async () => { async () => {
const { resetDefaultProfileState: currentResetDefaultProfileState } = const { resetDefaultProfileState: currentResetDefaultProfileState } =
internalStateContainer.getState(); internalState.getState();
if (currentResetDefaultProfileState.resetId !== resetDefaultProfileState.resetId) { if (currentResetDefaultProfileState.resetId !== resetDefaultProfileState.resetId) {
return; return;
@ -313,11 +318,13 @@ export function getDataStateContainer({
// Clear the default profile state flags after the data fetching // Clear the default profile state flags after the data fetching
// is done so refetches don't reset the state again // is done so refetches don't reset the state again
internalStateContainer.transitions.setResetDefaultProfileState({ internalState.dispatch(
columns: false, internalStateActions.setResetDefaultProfileState({
rowHeight: false, columns: false,
breakdownField: false, rowHeight: false,
}); breakdownField: false,
})
);
} }
); );

View file

@ -1,228 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { v4 as uuidv4 } from 'uuid';
import {
createStateContainer,
createStateContainerReactHelpers,
ReduxLikeStateContainer,
} from '@kbn/kibana-utils-plugin/common';
import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/common';
import type { Filter, TimeRange } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
import { differenceBy } from 'lodash';
interface InternalStateDataRequestParams {
timeRangeAbsolute?: TimeRange;
timeRangeRelative?: TimeRange;
}
export interface InternalState {
dataView: DataView | undefined;
isDataViewLoading: boolean;
savedDataViews: DataViewListItem[];
adHocDataViews: DataView[];
defaultProfileAdHocDataViewIds: string[];
expandedDoc: DataTableRecord | undefined;
customFilters: Filter[];
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving
isESQLToDataViewTransitionModalVisible?: boolean;
resetDefaultProfileState: {
resetId: string;
columns: boolean;
rowHeight: boolean;
breakdownField: boolean;
};
dataRequestParams: InternalStateDataRequestParams;
}
export interface InternalStateTransitions {
setDataView: (state: InternalState) => (dataView: DataView) => InternalState;
setIsDataViewLoading: (state: InternalState) => (isLoading: boolean) => InternalState;
setSavedDataViews: (state: InternalState) => (dataView: DataViewListItem[]) => InternalState;
setAdHocDataViews: (state: InternalState) => (dataViews: DataView[]) => InternalState;
setDefaultProfileAdHocDataViews: (
state: InternalState
) => (dataViews: DataView[]) => InternalState;
appendAdHocDataViews: (
state: InternalState
) => (dataViews: DataView | DataView[]) => InternalState;
replaceAdHocDataViewWithId: (
state: InternalState
) => (id: string, dataView: DataView) => InternalState;
setExpandedDoc: (
state: InternalState
) => (dataView: DataTableRecord | undefined) => InternalState;
setCustomFilters: (state: InternalState) => (customFilters: Filter[]) => InternalState;
setOverriddenVisContextAfterInvalidation: (
state: InternalState
) => (
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined
) => InternalState;
resetOnSavedSearchChange: (state: InternalState) => () => InternalState;
setIsESQLToDataViewTransitionModalVisible: (
state: InternalState
) => (isVisible: boolean) => InternalState;
setResetDefaultProfileState: (
state: InternalState
) => (
resetDefaultProfileState: Omit<InternalState['resetDefaultProfileState'], 'resetId'>
) => InternalState;
setDataRequestParams: (
state: InternalState
) => (params: InternalStateDataRequestParams) => InternalState;
}
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
InternalState,
InternalStateTransitions
>;
export const { Provider: InternalStateProvider, useSelector: useInternalStateSelector } =
createStateContainerReactHelpers<ReduxLikeStateContainer<InternalState>>();
export function getInternalStateContainer() {
return createStateContainer<InternalState, InternalStateTransitions, {}>(
{
dataView: undefined,
isDataViewLoading: false,
adHocDataViews: [],
defaultProfileAdHocDataViewIds: [],
savedDataViews: [],
expandedDoc: undefined,
customFilters: [],
overriddenVisContextAfterInvalidation: undefined,
resetDefaultProfileState: {
resetId: '',
columns: false,
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
},
{
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
...prevState,
dataView: nextDataView,
expandedDoc:
nextDataView?.id !== prevState.dataView?.id ? undefined : prevState.expandedDoc,
}),
setIsDataViewLoading: (prevState: InternalState) => (loading: boolean) => ({
...prevState,
isDataViewLoading: loading,
}),
setIsESQLToDataViewTransitionModalVisible:
(prevState: InternalState) => (isVisible: boolean) => ({
...prevState,
isESQLToDataViewTransitionModalVisible: isVisible,
}),
setSavedDataViews: (prevState: InternalState) => (nextDataViewList: DataViewListItem[]) => ({
...prevState,
savedDataViews: nextDataViewList,
}),
setAdHocDataViews: (prevState: InternalState) => (newAdHocDataViewList: DataView[]) => ({
...prevState,
adHocDataViews: newAdHocDataViewList,
}),
setDefaultProfileAdHocDataViews:
(prevState: InternalState) => (defaultProfileAdHocDataViews: DataView[]) => {
const adHocDataViews = prevState.adHocDataViews
.filter((dataView) => !prevState.defaultProfileAdHocDataViewIds.includes(dataView.id!))
.concat(defaultProfileAdHocDataViews);
const defaultProfileAdHocDataViewIds = defaultProfileAdHocDataViews.map(
(dataView) => dataView.id!
);
return {
...prevState,
adHocDataViews,
defaultProfileAdHocDataViewIds,
};
},
appendAdHocDataViews:
(prevState: InternalState) => (dataViewsAdHoc: DataView | DataView[]) => {
const newDataViews = Array.isArray(dataViewsAdHoc) ? dataViewsAdHoc : [dataViewsAdHoc];
const existingDataViews = differenceBy(prevState.adHocDataViews, newDataViews, 'id');
return {
...prevState,
adHocDataViews: existingDataViews.concat(newDataViews),
};
},
replaceAdHocDataViewWithId:
(prevState: InternalState) => (prevId: string, newDataView: DataView) => {
let defaultProfileAdHocDataViewIds = prevState.defaultProfileAdHocDataViewIds;
if (defaultProfileAdHocDataViewIds.includes(prevId)) {
defaultProfileAdHocDataViewIds = defaultProfileAdHocDataViewIds.map((id) =>
id === prevId ? newDataView.id! : id
);
}
return {
...prevState,
adHocDataViews: prevState.adHocDataViews.map((dataView) =>
dataView.id === prevId ? newDataView : dataView
),
defaultProfileAdHocDataViewIds,
};
},
setExpandedDoc: (prevState: InternalState) => (expandedDoc: DataTableRecord | undefined) => ({
...prevState,
expandedDoc,
}),
setCustomFilters: (prevState: InternalState) => (customFilters: Filter[]) => ({
...prevState,
customFilters,
}),
setOverriddenVisContextAfterInvalidation:
(prevState: InternalState) =>
(overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined) => ({
...prevState,
overriddenVisContextAfterInvalidation,
}),
resetOnSavedSearchChange: (prevState: InternalState) => () => ({
...prevState,
overriddenVisContextAfterInvalidation: undefined,
expandedDoc: undefined,
}),
setDataRequestParams:
(prevState: InternalState) => (params: InternalStateDataRequestParams) => ({
...prevState,
dataRequestParams: params,
}),
setResetDefaultProfileState:
(prevState: InternalState) =>
(resetDefaultProfileState: Omit<InternalState['resetDefaultProfileState'], 'resetId'>) => ({
...prevState,
resetDefaultProfileState: {
...resetDefaultProfileState,
resetId: uuidv4(),
},
}),
},
{},
{ freeze: (state) => state }
);
}
export const selectDataViewsForPicker = ({
savedDataViews,
adHocDataViews: originalAdHocDataViews,
defaultProfileAdHocDataViewIds,
}: InternalState) => {
const managedDataViews = originalAdHocDataViews.filter(
({ id }) => id && defaultProfileAdHocDataViewIds.includes(id)
);
const adHocDataViews = differenceBy(originalAdHocDataViews, managedDataViews, 'id');
return { savedDataViews, managedDataViews, adHocDataViews };
};

View file

@ -17,20 +17,24 @@ import { getDiscoverGlobalStateContainer } from './discover_global_state_contain
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { VIEW_MODE } from '../../../../common/constants'; import { VIEW_MODE } from '../../../../common/constants';
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { getInternalStateContainer } from './discover_internal_state_container'; import { createInternalStateStore, createRuntimeStateManager } from './redux';
describe('DiscoverSavedSearchContainer', () => { describe('DiscoverSavedSearchContainer', () => {
const savedSearch = savedSearchMock; const savedSearch = savedSearchMock;
const services = discoverServiceMock; const services = discoverServiceMock;
const globalStateContainer = getDiscoverGlobalStateContainer(createKbnUrlStateStorage()); const globalStateContainer = getDiscoverGlobalStateContainer(createKbnUrlStateStorage());
const internalStateContainer = getInternalStateContainer(); const internalState = createInternalStateStore({
services,
runtimeStateManager: createRuntimeStateManager(),
});
describe('getTitle', () => { describe('getTitle', () => {
it('returns undefined for new saved searches', () => { it('returns undefined for new saved searches', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
expect(container.getTitle()).toBe(undefined); expect(container.getTitle()).toBe(undefined);
}); });
@ -39,7 +43,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
container.set(savedSearch); container.set(savedSearch);
expect(container.getTitle()).toBe(savedSearch.title); expect(container.getTitle()).toBe(savedSearch.title);
@ -51,7 +56,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' }; const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
const result = container.set(newSavedSearch); const result = container.set(newSavedSearch);
@ -68,7 +74,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' }; const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
@ -82,7 +89,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const result = await container.new(dataViewMock); const result = await container.new(dataViewMock);
@ -99,7 +107,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({ const container = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const result = await container.new(dataViewMock); const result = await container.new(dataViewMock);
expect(result.title).toBeUndefined(); expect(result.title).toBeUndefined();
@ -119,7 +128,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
await savedSearchContainer.load('the-saved-search-id'); await savedSearchContainer.load('the-saved-search-id');
expect(savedSearchContainer.getInitial$().getValue().id).toEqual('the-saved-search-id'); expect(savedSearchContainer.getInitial$().getValue().id).toEqual('the-saved-search-id');
@ -135,7 +145,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const savedSearchToPersist = { const savedSearchToPersist = {
...savedSearchMockWithTimeField, ...savedSearchMockWithTimeField,
@ -161,7 +172,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const result = await savedSearchContainer.persist(persistedSavedSearch, saveOptions); const result = await savedSearchContainer.persist(persistedSavedSearch, saveOptions);
@ -174,7 +186,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const savedSearchToPersist = { const savedSearchToPersist = {
...savedSearchMockWithTimeField, ...savedSearchMockWithTimeField,
@ -194,7 +207,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const savedSearchToPersist = { const savedSearchToPersist = {
...savedSearchMockWithTimeField, ...savedSearchMockWithTimeField,
@ -219,7 +233,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
savedSearchContainer.set(savedSearch); savedSearchContainer.set(savedSearch);
savedSearchContainer.update({ nextState: { hideChart: true } }); savedSearchContainer.update({ nextState: { hideChart: true } });
@ -241,7 +256,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
savedSearchContainer.set(savedSearch); savedSearchContainer.set(savedSearch);
const updated = savedSearchContainer.update({ nextState: { hideChart: true } }); const updated = savedSearchContainer.update({ nextState: { hideChart: true } });
@ -257,7 +273,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock, services: discoverServiceMock,
globalStateContainer, globalStateContainer,
internalStateContainer,
internalState,
}); });
const updated = savedSearchContainer.update({ nextDataView: dataViewMock }); const updated = savedSearchContainer.update({ nextDataView: dataViewMock });
expect(savedSearchContainer.getHasChanged$().getValue()).toBe(true); expect(savedSearchContainer.getHasChanged$().getValue()).toBe(true);

View file

@ -30,7 +30,7 @@ import { DiscoverAppState, isEqualFilters } from './discover_app_state_container
import { DiscoverServices } from '../../../build_services'; import { DiscoverServices } from '../../../build_services';
import { getStateDefaults } from './utils/get_state_defaults'; import { getStateDefaults } from './utils/get_state_defaults';
import type { DiscoverGlobalStateContainer } from './discover_global_state_container'; import type { DiscoverGlobalStateContainer } from './discover_global_state_container';
import type { DiscoverInternalStateContainer } from './discover_internal_state_container'; import { InternalStateStore } from './redux';
const FILTERS_COMPARE_OPTIONS: FilterCompareOptions = { const FILTERS_COMPARE_OPTIONS: FilterCompareOptions = {
...COMPARE_ALL_OPTIONS, ...COMPARE_ALL_OPTIONS,
@ -139,11 +139,11 @@ export interface DiscoverSavedSearchContainer {
export function getSavedSearchContainer({ export function getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer, internalState,
}: { }: {
services: DiscoverServices; services: DiscoverServices;
globalStateContainer: DiscoverGlobalStateContainer; globalStateContainer: DiscoverGlobalStateContainer;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
}): DiscoverSavedSearchContainer { }): DiscoverSavedSearchContainer {
const initialSavedSearch = services.savedSearch.getNew(); const initialSavedSearch = services.savedSearch.getNew();
const savedSearchInitial$ = new BehaviorSubject(initialSavedSearch); const savedSearchInitial$ = new BehaviorSubject(initialSavedSearch);
@ -183,7 +183,7 @@ export function getSavedSearchContainer({
addLog('[savedSearch] persist', { nextSavedSearch, saveOptions }); addLog('[savedSearch] persist', { nextSavedSearch, saveOptions });
const dataView = nextSavedSearch.searchSource.getField('index'); const dataView = nextSavedSearch.searchSource.getField('index');
const profileDataViewIds = internalStateContainer.getState().defaultProfileAdHocDataViewIds; const profileDataViewIds = internalState.getState().defaultProfileAdHocDataViewIds;
let replacementDataView: DataView | undefined; let replacementDataView: DataView | undefined;
// If the Discover session is using a default profile ad hoc data view, // If the Discover session is using a default profile ad hoc data view,

View file

@ -32,6 +32,7 @@ import { copySavedSearch } from './discover_saved_search_container';
import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context'; import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context';
import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources'; import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources';
import { createRuntimeStateManager } from './redux';
const startSync = (appState: DiscoverAppStateContainer) => { const startSync = (appState: DiscoverAppStateContainer) => {
const { start, stop } = appState.syncState(); const { start, stop } = appState.syncState();
@ -46,17 +47,22 @@ async function getState(
const nextHistory = createBrowserHistory(); const nextHistory = createBrowserHistory();
nextHistory.push(url); nextHistory.push(url);
discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({ discoverServiceMock.dataViews.create = jest.fn().mockImplementation((spec) => {
...dataViewMock, spec.id = spec.id ?? 'ad-hoc-id';
isPersisted: () => false, spec.title = spec.title ?? 'test';
id: 'ad-hoc-id', return Promise.resolve({
title: 'test', ...dataViewMock,
isPersisted: () => false,
toSpec: () => spec,
...spec,
});
}); });
const runtimeStateManager = createRuntimeStateManager();
const nextState = getDiscoverStateContainer({ const nextState = getDiscoverStateContainer({
services: discoverServiceMock, services: discoverServiceMock,
history: nextHistory, history: nextHistory,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
runtimeStateManager,
}); });
nextState.appState.isEmptyURL = jest.fn(() => isEmptyUrl ?? true); nextState.appState.isEmptyURL = jest.fn(() => isEmptyUrl ?? true);
jest.spyOn(nextState.dataState, 'fetch'); jest.spyOn(nextState.dataState, 'fetch');
@ -77,6 +83,7 @@ async function getState(
return { return {
history: nextHistory, history: nextHistory,
state: nextState, state: nextState,
runtimeStateManager,
getCurrentUrl, getCurrentUrl,
}; };
} }
@ -94,6 +101,7 @@ describe('Test discover state', () => {
services: discoverServiceMock, services: discoverServiceMock,
history, history,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
}); });
state.savedSearchState.set(savedSearchMock); state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true); state.appState.update({}, true);
@ -192,6 +200,7 @@ describe('Test discover state with overridden state storage', () => {
history, history,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
stateStorageContainer: stateStorage, stateStorageContainer: stateStorage,
runtimeStateManager: createRuntimeStateManager(),
}); });
state.savedSearchState.set(savedSearchMock); state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true); state.appState.update({}, true);
@ -283,6 +292,7 @@ describe('Test createSearchSessionRestorationDataProvider', () => {
services: discoverServiceMock, services: discoverServiceMock,
history, history,
customizationContext: mockCustomizationContext, customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
}); });
discoverStateContainer.appState.update({ discoverStateContainer.appState.update({
dataSource: createDataViewDataSource({ dataSource: createDataViewDataSource({
@ -419,9 +429,11 @@ describe('Test discover state actions', () => {
}); });
test('setDataView', async () => { test('setDataView', async () => {
const { state } = await getState(''); const { state, runtimeStateManager } = await getState('');
expect(runtimeStateManager.currentDataView$.getValue()).toBeUndefined();
state.actions.setDataView(dataViewMock); state.actions.setDataView(dataViewMock);
expect(state.internalState.getState().dataView).toBe(dataViewMock); expect(runtimeStateManager.currentDataView$.getValue()).toBe(dataViewMock);
expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id);
}); });
test('fetchData', async () => { test('fetchData', async () => {
@ -717,7 +729,7 @@ describe('Test discover state actions', () => {
state.savedSearchState.getCurrent$().getValue().searchSource?.getField('index')?.id state.savedSearchState.getCurrent$().getValue().searchSource?.getField('index')?.id
).toEqual(dataViewSpecMock.id); ).toEqual(dataViewSpecMock.id);
expect(state.savedSearchState.getHasChanged$().getValue()).toEqual(false); expect(state.savedSearchState.getHasChanged$().getValue()).toEqual(false);
expect(state.internalState.getState().adHocDataViews.length).toBe(1); expect(state.runtimeStateManager.adHocDataViews$.getValue().length).toBe(1);
}); });
test('loadSavedSearch resetting query & filters of data service', async () => { test('loadSavedSearch resetting query & filters of data service', async () => {
@ -749,7 +761,7 @@ describe('Test discover state actions', () => {
expect(state.appState.getState().dataSource).toEqual( expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: adHocDataViewId! }) createDataViewDataSource({ dataViewId: adHocDataViewId! })
); );
expect(state.internalState.getState().adHocDataViews[0].id).toBe(adHocDataViewId); expect(state.runtimeStateManager.adHocDataViews$.getValue()[0].id).toBe(adHocDataViewId);
}); });
test('loadSavedSearch with ES|QL, data view index is not overwritten by URL ', async () => { test('loadSavedSearch with ES|QL, data view index is not overwritten by URL ', async () => {
@ -829,7 +841,7 @@ describe('Test discover state actions', () => {
const unsubscribe = state.actions.initializeAndSync(); const unsubscribe = state.actions.initializeAndSync();
await state.actions.onDataViewCreated(dataViewComplexMock); await state.actions.onDataViewCreated(dataViewComplexMock);
await waitFor(() => { await waitFor(() => {
expect(state.internalState.getState().dataView?.id).toBe(dataViewComplexMock.id); expect(state.internalState.getState().dataViewId).toBe(dataViewComplexMock.id);
}); });
expect(state.appState.getState().dataSource).toEqual( expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }) createDataViewDataSource({ dataViewId: dataViewComplexMock.id! })
@ -844,9 +856,14 @@ describe('Test discover state actions', () => {
const { state } = await getState('/', { savedSearch: savedSearchMock }); const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
const unsubscribe = state.actions.initializeAndSync(); const unsubscribe = state.actions.initializeAndSync();
jest
.spyOn(discoverServiceMock.dataViews, 'get')
.mockImplementationOnce((id) =>
id === dataViewAdHoc.id ? Promise.resolve(dataViewAdHoc) : Promise.reject()
);
await state.actions.onDataViewCreated(dataViewAdHoc); await state.actions.onDataViewCreated(dataViewAdHoc);
await waitFor(() => { await waitFor(() => {
expect(state.internalState.getState().dataView?.id).toBe(dataViewAdHoc.id); expect(state.internalState.getState().dataViewId).toBe(dataViewAdHoc.id);
}); });
expect(state.appState.getState().dataSource).toEqual( expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: dataViewAdHoc.id! }) createDataViewDataSource({ dataViewId: dataViewAdHoc.id! })
@ -860,15 +877,12 @@ describe('Test discover state actions', () => {
test('onDataViewEdited - persisted data view', async () => { test('onDataViewEdited - persisted data view', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock }); const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
const selectedDataView = state.internalState.getState().dataView; const selectedDataViewId = state.internalState.getState().dataViewId;
await waitFor(() => { expect(selectedDataViewId).toBe(dataViewMock.id);
expect(selectedDataView).toBe(dataViewMock);
});
const unsubscribe = state.actions.initializeAndSync(); const unsubscribe = state.actions.initializeAndSync();
await state.actions.onDataViewEdited(dataViewMock); await state.actions.onDataViewEdited(dataViewMock);
await waitFor(() => { await waitFor(() => {
expect(state.internalState.getState().dataView).not.toBe(selectedDataView); expect(state.internalState.getState().dataViewId).toBe(selectedDataViewId);
}); });
unsubscribe(); unsubscribe();
}); });
@ -880,7 +894,7 @@ describe('Test discover state actions', () => {
const previousId = dataViewAdHoc.id; const previousId = dataViewAdHoc.id;
await state.actions.onDataViewEdited(dataViewAdHoc); await state.actions.onDataViewEdited(dataViewAdHoc);
await waitFor(() => { await waitFor(() => {
expect(state.internalState.getState().dataView?.id).not.toBe(previousId); expect(state.internalState.getState().dataViewId).not.toBe(previousId);
}); });
unsubscribe(); unsubscribe();
}); });
@ -916,7 +930,7 @@ describe('Test discover state actions', () => {
expect(state.appState.getState().dataSource).toEqual( expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: 'ad-hoc-id' }) createDataViewDataSource({ dataViewId: 'ad-hoc-id' })
); );
expect(state.internalState.getState().adHocDataViews[0].id).toBe('ad-hoc-id'); expect(state.runtimeStateManager.adHocDataViews$.getValue()[0].id).toBe('ad-hoc-id');
unsubscribe(); unsubscribe();
}); });
@ -929,7 +943,7 @@ describe('Test discover state actions', () => {
const initialUrlState = 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:!())'; '/#?_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(getCurrentUrl()).toBe(initialUrlState);
expect(state.internalState.getState().dataView?.id).toBe(dataViewMock.id!); expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id!);
// Change the data view, this should change the URL and trigger a fetch // Change the data view, this should change the URL and trigger a fetch
await state.actions.onChangeDataView(dataViewComplexMock.id!); await state.actions.onChangeDataView(dataViewComplexMock.id!);
@ -940,7 +954,7 @@ describe('Test discover state actions', () => {
await waitFor(() => { await waitFor(() => {
expect(state.dataState.fetch).toHaveBeenCalledTimes(1); expect(state.dataState.fetch).toHaveBeenCalledTimes(1);
}); });
expect(state.internalState.getState().dataView?.id).toBe(dataViewComplexMock.id!); expect(state.internalState.getState().dataViewId).toBe(dataViewComplexMock.id!);
// Undo all changes to the saved search, this should trigger a fetch, again // Undo all changes to the saved search, this should trigger a fetch, again
await state.actions.undoSavedSearchChanges(); await state.actions.undoSavedSearchChanges();
@ -949,7 +963,7 @@ describe('Test discover state actions', () => {
await waitFor(() => { await waitFor(() => {
expect(state.dataState.fetch).toHaveBeenCalledTimes(2); expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
}); });
expect(state.internalState.getState().dataView?.id).toBe(dataViewMock.id!); expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id!);
unsubscribe(); unsubscribe();
}); });
@ -991,6 +1005,7 @@ describe('Test discover state with embedded mode', () => {
...mockCustomizationContext, ...mockCustomizationContext,
displayMode: 'embedded', displayMode: 'embedded',
}, },
runtimeStateManager: createRuntimeStateManager(),
}); });
state.savedSearchState.set(savedSearchMock); state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true); state.appState.update({}, true);

View file

@ -47,10 +47,6 @@ import {
DiscoverAppStateContainer, DiscoverAppStateContainer,
getDiscoverAppStateContainer, getDiscoverAppStateContainer,
} from './discover_app_state_container'; } from './discover_app_state_container';
import {
DiscoverInternalStateContainer,
getInternalStateContainer,
} from './discover_internal_state_container';
import { DiscoverServices } from '../../../build_services'; import { DiscoverServices } from '../../../build_services';
import { import {
getDefaultAppState, getDefaultAppState,
@ -68,6 +64,12 @@ import {
DataSourceType, DataSourceType,
isDataSourceType, isDataSourceType,
} from '../../../../common/data_sources'; } from '../../../../common/data_sources';
import {
createInternalStateStore,
internalStateActions,
InternalStateStore,
RuntimeStateManager,
} from './redux';
export interface DiscoverStateContainerParams { export interface DiscoverStateContainerParams {
/** /**
@ -90,6 +92,10 @@ export interface DiscoverStateContainerParams {
* a custom url state storage * a custom url state storage
*/ */
stateStorageContainer?: IKbnUrlStateStorage; stateStorageContainer?: IKbnUrlStateStorage;
/**
* State manager for runtime state that can't be stored in Redux
*/
runtimeStateManager: RuntimeStateManager;
} }
export interface LoadParams { export interface LoadParams {
@ -127,7 +133,11 @@ export interface DiscoverStateContainer {
/** /**
* Internal shared state that's used at several places in the UI * Internal shared state that's used at several places in the UI
*/ */
internalState: DiscoverInternalStateContainer; internalState: InternalStateStore;
/**
* State manager for runtime state that can't be stored in Redux
*/
runtimeStateManager: RuntimeStateManager;
/** /**
* State of saved search, the saved object of Discover * State of saved search, the saved object of Discover
*/ */
@ -242,6 +252,7 @@ export function getDiscoverStateContainer({
services, services,
customizationContext, customizationContext,
stateStorageContainer, stateStorageContainer,
runtimeStateManager,
}: DiscoverStateContainerParams): DiscoverStateContainer { }: DiscoverStateContainerParams): DiscoverStateContainer {
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
const toasts = services.core.notifications.toasts; const toasts = services.core.notifications.toasts;
@ -272,9 +283,9 @@ export function getDiscoverStateContainer({
const globalStateContainer = getDiscoverGlobalStateContainer(stateStorage); const globalStateContainer = getDiscoverGlobalStateContainer(stateStorage);
/** /**
* Internal State Container, state that's not persisted and not part of the URL * Internal state store, state that's not persisted and not part of the URL
*/ */
const internalStateContainer = getInternalStateContainer(); const internalState = createInternalStateStore({ services, runtimeStateManager });
/** /**
* Saved Search State Container, the persisted saved object of Discover * Saved Search State Container, the persisted saved object of Discover
@ -282,7 +293,7 @@ export function getDiscoverStateContainer({
const savedSearchContainer = getSavedSearchContainer({ const savedSearchContainer = getSavedSearchContainer({
services, services,
globalStateContainer, globalStateContainer,
internalStateContainer, internalState,
}); });
/** /**
@ -290,7 +301,7 @@ export function getDiscoverStateContainer({
*/ */
const appStateContainer = getDiscoverAppStateContainer({ const appStateContainer = getDiscoverAppStateContainer({
stateStorage, stateStorage,
internalStateContainer, internalState,
savedSearchContainer, savedSearchContainer,
services, services,
}); });
@ -308,7 +319,7 @@ export function getDiscoverStateContainer({
}; };
const setDataView = (dataView: DataView) => { const setDataView = (dataView: DataView) => {
internalStateContainer.transitions.setDataView(dataView); internalState.dispatch(internalStateActions.setDataView(dataView));
pauseAutoRefreshInterval(dataView); pauseAutoRefreshInterval(dataView);
savedSearchContainer.getState().searchSource.setField('index', dataView); savedSearchContainer.getState().searchSource.setField('index', dataView);
}; };
@ -317,14 +328,15 @@ export function getDiscoverStateContainer({
services, services,
searchSessionManager, searchSessionManager,
appStateContainer, appStateContainer,
internalStateContainer, internalState,
runtimeStateManager,
getSavedSearch: savedSearchContainer.getState, getSavedSearch: savedSearchContainer.getState,
setDataView, setDataView,
}); });
const loadDataViewList = async () => { const loadDataViewList = async () => {
const dataViewList = await services.dataViews.getIdsWithTitle(true); const savedDataViews = await services.dataViews.getIdsWithTitle(true);
internalStateContainer.transitions.setSavedDataViews(dataViewList); internalState.dispatch(internalStateActions.setSavedDataViews(savedDataViews));
}; };
/** /**
@ -332,7 +344,7 @@ export function getDiscoverStateContainer({
* This is to prevent duplicate ids messing with our system * This is to prevent duplicate ids messing with our system
*/ */
const updateAdHocDataViewId = async () => { const updateAdHocDataViewId = async () => {
const prevDataView = internalStateContainer.getState().dataView; const prevDataView = runtimeStateManager.currentDataView$.getValue();
if (!prevDataView || prevDataView.isPersisted()) return; if (!prevDataView || prevDataView.isPersisted()) return;
const nextDataView = await services.dataViews.create({ const nextDataView = await services.dataViews.create({
@ -348,7 +360,9 @@ export function getDiscoverStateContainer({
services, services,
}); });
internalStateContainer.transitions.replaceAdHocDataViewWithId(prevDataView.id!, nextDataView); internalState.dispatch(
internalStateActions.replaceAdHocDataViewWithId(prevDataView.id!, nextDataView)
);
if (isDataSourceType(appStateContainer.get().dataSource, DataSourceType.DataView)) { if (isDataSourceType(appStateContainer.get().dataSource, DataSourceType.DataView)) {
await appStateContainer.replaceUrlState({ await appStateContainer.replaceUrlState({
@ -413,7 +427,7 @@ export function getDiscoverStateContainer({
const onDataViewCreated = async (nextDataView: DataView) => { const onDataViewCreated = async (nextDataView: DataView) => {
if (!nextDataView.isPersisted()) { if (!nextDataView.isPersisted()) {
internalStateContainer.transitions.appendAdHocDataViews(nextDataView); internalState.dispatch(internalStateActions.appendAdHocDataViews(nextDataView));
} else { } else {
await loadDataViewList(); await loadDataViewList();
} }
@ -440,7 +454,8 @@ export function getDiscoverStateContainer({
return loadSavedSearchFn(params ?? {}, { return loadSavedSearchFn(params ?? {}, {
appStateContainer, appStateContainer,
dataStateContainer, dataStateContainer,
internalStateContainer, internalState,
runtimeStateManager,
savedSearchContainer, savedSearchContainer,
globalStateContainer, globalStateContainer,
services, services,
@ -470,7 +485,8 @@ export function getDiscoverStateContainer({
appState: appStateContainer, appState: appStateContainer,
savedSearchState: savedSearchContainer, savedSearchState: savedSearchContainer,
dataState: dataStateContainer, dataState: dataStateContainer,
internalState: internalStateContainer, internalState,
runtimeStateManager,
services, services,
setDataView, setDataView,
}) })
@ -482,7 +498,7 @@ export function getDiscoverStateContainer({
// updates saved search when query or filters change, triggers data fetching // updates saved search when query or filters change, triggers data fetching
const filterUnsubscribe = merge(services.filterManager.getFetches$()).subscribe(() => { const filterUnsubscribe = merge(services.filterManager.getFetches$()).subscribe(() => {
savedSearchContainer.update({ savedSearchContainer.update({
nextDataView: internalStateContainer.getState().dataView, nextDataView: runtimeStateManager.currentDataView$.getValue(),
nextState: appStateContainer.getState(), nextState: appStateContainer.getState(),
useFilterAndQueryServices: true, useFilterAndQueryServices: true,
}); });
@ -521,8 +537,7 @@ export function getDiscoverStateContainer({
if (newDataView.fields.getByName('@timestamp')?.type === 'date') { if (newDataView.fields.getByName('@timestamp')?.type === 'date') {
newDataView.timeFieldName = '@timestamp'; newDataView.timeFieldName = '@timestamp';
} }
internalStateContainer.transitions.appendAdHocDataViews(newDataView); internalState.dispatch(internalStateActions.appendAdHocDataViews(newDataView));
await onChangeDataView(newDataView); await onChangeDataView(newDataView);
return newDataView; return newDataView;
}; };
@ -545,10 +560,12 @@ export function getDiscoverStateContainer({
/** /**
* Function e.g. triggered when user changes data view in the sidebar * Function e.g. triggered when user changes data view in the sidebar
*/ */
const onChangeDataView = async (id: string | DataView) => { const onChangeDataView = async (dataViewId: string | DataView) => {
await changeDataView(id, { await changeDataView({
dataViewId,
services, services,
internalState: internalStateContainer, internalState,
runtimeStateManager,
appState: appStateContainer, appState: appStateContainer,
}); });
}; };
@ -575,7 +592,7 @@ export function getDiscoverStateContainer({
}); });
} }
internalStateContainer.transitions.resetOnSavedSearchChange(); internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
await appStateContainer.replaceUrlState(newAppState); await appStateContainer.replaceUrlState(newAppState);
return nextSavedSearch; return nextSavedSearch;
}; };
@ -606,7 +623,8 @@ export function getDiscoverStateContainer({
return { return {
globalState: globalStateContainer, globalState: globalStateContainer,
appState: appStateContainer, appState: appStateContainer,
internalState: internalStateContainer, internalState,
runtimeStateManager,
dataState: dataStateContainer, dataState: dataStateContainer,
savedSearchState: savedSearchContainer, savedSearchState: savedSearchContainer,
stateStorage, stateStorage,

View file

@ -10,9 +10,9 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { InternalStateProvider } from './discover_internal_state_container';
import { DiscoverAppStateProvider } from './discover_app_state_container'; import { DiscoverAppStateProvider } from './discover_app_state_container';
import { DiscoverStateContainer } from './discover_state'; import { DiscoverStateContainer } from './discover_state';
import { InternalStateProvider } from './redux';
function createStateHelpers() { function createStateHelpers() {
const context = React.createContext<DiscoverStateContainer | null>(null); const context = React.createContext<DiscoverStateContainer | null>(null);
@ -63,7 +63,7 @@ export const DiscoverMainProvider = ({
return ( return (
<DiscoverStateProvider value={value}> <DiscoverStateProvider value={value}>
<DiscoverAppStateProvider value={value.appState}> <DiscoverAppStateProvider value={value.appState}>
<InternalStateProvider value={value.internalState}>{children}</InternalStateProvider> <InternalStateProvider store={value.internalState}>{children}</InternalStateProvider>
</DiscoverAppStateProvider> </DiscoverAppStateProvider>
</DiscoverStateProvider> </DiscoverStateProvider>
); );

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataView } from '@kbn/data-views-plugin/common';
import { differenceBy } from 'lodash';
import { internalStateSlice, type InternalStateThunkActionCreator } from '../internal_state';
export const setDataView: InternalStateThunkActionCreator<[DataView]> =
(dataView) =>
(dispatch, _, { runtimeStateManager }) => {
dispatch(internalStateSlice.actions.setDataViewId(dataView.id));
runtimeStateManager.currentDataView$.next(dataView);
};
export const setAdHocDataViews: InternalStateThunkActionCreator<[DataView[]]> =
(adHocDataViews) =>
(_, __, { runtimeStateManager }) => {
runtimeStateManager.adHocDataViews$.next(adHocDataViews);
};
export const setDefaultProfileAdHocDataViews: InternalStateThunkActionCreator<[DataView[]]> =
(defaultProfileAdHocDataViews) =>
(dispatch, getState, { runtimeStateManager }) => {
const prevAdHocDataViews = runtimeStateManager.adHocDataViews$.getValue();
const prevState = getState();
const adHocDataViews = prevAdHocDataViews
.filter((dataView) => !prevState.defaultProfileAdHocDataViewIds.includes(dataView.id!))
.concat(defaultProfileAdHocDataViews);
const defaultProfileAdHocDataViewIds = defaultProfileAdHocDataViews.map(
(dataView) => dataView.id!
);
dispatch(setAdHocDataViews(adHocDataViews));
dispatch(
internalStateSlice.actions.setDefaultProfileAdHocDataViewIds(defaultProfileAdHocDataViewIds)
);
};
export const appendAdHocDataViews: InternalStateThunkActionCreator<[DataView | DataView[]]> =
(dataViewsAdHoc) =>
(dispatch, _, { runtimeStateManager }) => {
const prevAdHocDataViews = runtimeStateManager.adHocDataViews$.getValue();
const newDataViews = Array.isArray(dataViewsAdHoc) ? dataViewsAdHoc : [dataViewsAdHoc];
const existingDataViews = differenceBy(prevAdHocDataViews, newDataViews, 'id');
dispatch(setAdHocDataViews(existingDataViews.concat(newDataViews)));
};
export const replaceAdHocDataViewWithId: InternalStateThunkActionCreator<[string, DataView]> =
(prevId, newDataView) =>
(dispatch, getState, { runtimeStateManager }) => {
const prevAdHocDataViews = runtimeStateManager.adHocDataViews$.getValue();
let defaultProfileAdHocDataViewIds = getState().defaultProfileAdHocDataViewIds;
if (defaultProfileAdHocDataViewIds.includes(prevId)) {
defaultProfileAdHocDataViewIds = defaultProfileAdHocDataViewIds.map((id) =>
id === prevId ? newDataView.id! : id
);
}
dispatch(
setAdHocDataViews(
prevAdHocDataViews.map((dataView) => (dataView.id === prevId ? newDataView : dataView))
)
);
dispatch(
internalStateSlice.actions.setDefaultProfileAdHocDataViewIds(defaultProfileAdHocDataViewIds)
);
};

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './data_views';

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { differenceBy } from 'lodash';
import {
type TypedUseSelectorHook,
type ReactReduxContextValue,
Provider as ReduxProvider,
createDispatchHook,
createSelectorHook,
} from 'react-redux';
import React, { type PropsWithChildren, useMemo, createContext } from 'react';
import { useAdHocDataViews } from './runtime_state';
import type { DiscoverInternalState } from './types';
import type { InternalStateDispatch, InternalStateStore } from './internal_state';
const internalStateContext = createContext<ReactReduxContextValue>(
// Recommended approach for versions of Redux prior to v9:
// https://github.com/reduxjs/react-redux/issues/1565#issuecomment-867143221
null as unknown as ReactReduxContextValue
);
export const InternalStateProvider = ({
store,
children,
}: PropsWithChildren<{ store: InternalStateStore }>) => (
<ReduxProvider store={store} context={internalStateContext}>
{children}
</ReduxProvider>
);
export const useInternalStateDispatch: () => InternalStateDispatch =
createDispatchHook(internalStateContext);
export const useInternalStateSelector: TypedUseSelectorHook<DiscoverInternalState> =
createSelectorHook(internalStateContext);
export const useDataViewsForPicker = () => {
const originalAdHocDataViews = useAdHocDataViews();
const savedDataViews = useInternalStateSelector((state) => state.savedDataViews);
const defaultProfileAdHocDataViewIds = useInternalStateSelector(
(state) => state.defaultProfileAdHocDataViewIds
);
return useMemo(() => {
const managedDataViews = originalAdHocDataViews.filter(
({ id }) => id && defaultProfileAdHocDataViewIds.includes(id)
);
const adHocDataViews = differenceBy(originalAdHocDataViews, managedDataViews, 'id');
return { savedDataViews, managedDataViews, adHocDataViews };
}, [defaultProfileAdHocDataViewIds, originalAdHocDataViews, savedDataViews]);
};

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import { internalStateSlice } from './internal_state';
import {
appendAdHocDataViews,
replaceAdHocDataViewWithId,
setAdHocDataViews,
setDataView,
setDefaultProfileAdHocDataViews,
} from './actions';
export type { DiscoverInternalState, InternalStateDataRequestParams } from './types';
export { type InternalStateStore, createInternalStateStore } from './internal_state';
export const internalStateActions = {
...omit(internalStateSlice.actions, 'setDataViewId', 'setDefaultProfileAdHocDataViewIds'),
setDataView,
setAdHocDataViews,
setDefaultProfileAdHocDataViews,
appendAdHocDataViews,
replaceAdHocDataViewWithId,
};
export {
InternalStateProvider,
useInternalStateDispatch,
useInternalStateSelector,
useDataViewsForPicker,
} from './hooks';
export {
type RuntimeStateManager,
createRuntimeStateManager,
useRuntimeState,
RuntimeStateProvider,
useCurrentDataView,
useAdHocDataViews,
} from './runtime_state';

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
import { createInternalStateStore, createRuntimeStateManager, internalStateActions } from '.';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
describe('InternalStateStore', () => {
it('should set data view', () => {
const runtimeStateManager = createRuntimeStateManager();
const store = createInternalStateStore({
services: createDiscoverServicesMock(),
runtimeStateManager,
});
expect(store.getState().dataViewId).toBeUndefined();
expect(runtimeStateManager.currentDataView$.value).toBeUndefined();
store.dispatch(internalStateActions.setDataView(dataViewMock));
expect(store.getState().dataViewId).toBe(dataViewMock.id);
expect(runtimeStateManager.currentDataView$.value).toBe(dataViewMock);
});
});

View file

@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils';
import { v4 as uuidv4 } from 'uuid';
import {
type PayloadAction,
configureStore,
createSlice,
type ThunkAction,
type ThunkDispatch,
} from '@reduxjs/toolkit';
import type { DiscoverServices } from '../../../../build_services';
import type { RuntimeStateManager } from './runtime_state';
import type { DiscoverInternalState, InternalStateDataRequestParams } from './types';
const initialState: DiscoverInternalState = {
dataViewId: undefined,
isDataViewLoading: false,
defaultProfileAdHocDataViewIds: [],
savedDataViews: [],
expandedDoc: undefined,
dataRequestParams: {},
overriddenVisContextAfterInvalidation: undefined,
isESQLToDataViewTransitionModalVisible: false,
resetDefaultProfileState: {
resetId: '',
columns: false,
rowHeight: false,
breakdownField: false,
},
};
export const internalStateSlice = createSlice({
name: 'internalState',
initialState,
reducers: {
setDataViewId: (state, action: PayloadAction<string | undefined>) => {
if (action.payload !== state.dataViewId) {
state.expandedDoc = undefined;
}
state.dataViewId = action.payload;
},
setIsDataViewLoading: (state, action: PayloadAction<boolean>) => {
state.isDataViewLoading = action.payload;
},
setDefaultProfileAdHocDataViewIds: (state, action: PayloadAction<string[]>) => {
state.defaultProfileAdHocDataViewIds = action.payload;
},
setSavedDataViews: (state, action: PayloadAction<DataViewListItem[]>) => {
state.savedDataViews = action.payload;
},
setExpandedDoc: (state, action: PayloadAction<DataTableRecord | undefined>) => {
state.expandedDoc = action.payload;
},
setDataRequestParams: (state, action: PayloadAction<InternalStateDataRequestParams>) => {
state.dataRequestParams = action.payload;
},
setOverriddenVisContextAfterInvalidation: (
state,
action: PayloadAction<DiscoverInternalState['overriddenVisContextAfterInvalidation']>
) => {
state.overriddenVisContextAfterInvalidation = action.payload;
},
setIsESQLToDataViewTransitionModalVisible: (state, action: PayloadAction<boolean>) => {
state.isESQLToDataViewTransitionModalVisible = action.payload;
},
setResetDefaultProfileState: {
prepare: (
resetDefaultProfileState: Omit<DiscoverInternalState['resetDefaultProfileState'], 'resetId'>
) => ({
payload: {
...resetDefaultProfileState,
resetId: uuidv4(),
},
}),
reducer: (
state,
action: PayloadAction<DiscoverInternalState['resetDefaultProfileState']>
) => {
state.resetDefaultProfileState = action.payload;
},
},
resetOnSavedSearchChange: (state) => {
state.overriddenVisContextAfterInvalidation = undefined;
state.expandedDoc = undefined;
},
},
});
interface InternalStateThunkDependencies {
services: DiscoverServices;
runtimeStateManager: RuntimeStateManager;
}
export const createInternalStateStore = (options: InternalStateThunkDependencies) =>
configureStore({
reducer: internalStateSlice.reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: { extraArgument: options } }),
});
export type InternalStateStore = ReturnType<typeof createInternalStateStore>;
export type InternalStateDispatch = InternalStateStore['dispatch'];
type InternalStateThunkAction<TReturn = void> = ThunkAction<
TReturn,
InternalStateDispatch extends ThunkDispatch<infer TState, never, never> ? TState : never,
InternalStateDispatch extends ThunkDispatch<never, infer TExtra, never> ? TExtra : never,
InternalStateDispatch extends ThunkDispatch<never, never, infer TAction> ? TAction : never
>;
export type InternalStateThunkActionCreator<TArgs extends unknown[] = [], TReturn = void> = (
...args: TArgs
) => InternalStateThunkAction<TReturn>;

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataView } from '@kbn/data-views-plugin/common';
import React, { type PropsWithChildren, createContext, useContext, useMemo, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { BehaviorSubject, skip } from 'rxjs';
interface DiscoverRuntimeState {
currentDataView: DataView;
adHocDataViews: DataView[];
}
type RuntimeStateManagerInternal<TNullable extends keyof DiscoverRuntimeState> = {
[key in keyof DiscoverRuntimeState as `${key}$`]: BehaviorSubject<
key extends TNullable ? DiscoverRuntimeState[key] | undefined : DiscoverRuntimeState[key]
>;
};
export type RuntimeStateManager = RuntimeStateManagerInternal<'currentDataView'>;
export const createRuntimeStateManager = (): RuntimeStateManager => ({
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
adHocDataViews$: new BehaviorSubject<DataView[]>([]),
});
export const useRuntimeState = <T,>(stateSubject$: BehaviorSubject<T>) => {
const [stateObservable$] = useState(() => stateSubject$.pipe(skip(1)));
return useObservable(stateObservable$, stateSubject$.getValue());
};
const runtimeStateContext = createContext<DiscoverRuntimeState | undefined>(undefined);
export const RuntimeStateProvider = ({
currentDataView,
adHocDataViews,
children,
}: PropsWithChildren<DiscoverRuntimeState>) => {
const runtimeState = useMemo<DiscoverRuntimeState>(
() => ({ currentDataView, adHocDataViews }),
[adHocDataViews, currentDataView]
);
return (
<runtimeStateContext.Provider value={runtimeState}>{children}</runtimeStateContext.Provider>
);
};
const useRuntimeStateContext = () => {
const context = useContext(runtimeStateContext);
if (!context) {
throw new Error('useRuntimeStateContext must be used within a RuntimeStateProvider');
}
return context;
};
export const useCurrentDataView = () => useRuntimeStateContext().currentDataView;
export const useAdHocDataViews = () => useRuntimeStateContext().adHocDataViews;

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils';
import type { TimeRange } from '@kbn/es-query';
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
export interface InternalStateDataRequestParams {
timeRangeAbsolute?: TimeRange;
timeRangeRelative?: TimeRange;
}
export interface DiscoverInternalState {
dataViewId: string | undefined;
isDataViewLoading: boolean;
savedDataViews: DataViewListItem[];
defaultProfileAdHocDataViewIds: string[];
expandedDoc: DataTableRecord | undefined;
dataRequestParams: InternalStateDataRequestParams;
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving
isESQLToDataViewTransitionModalVisible: boolean;
resetDefaultProfileState: {
resetId: string;
columns: boolean;
rowHeight: boolean;
breakdownField: boolean;
};
}

View file

@ -30,6 +30,7 @@ describe('buildStateSubscribe', () => {
savedSearchState: stateContainer.savedSearchState, savedSearchState: stateContainer.savedSearchState,
dataState: stateContainer.dataState, dataState: stateContainer.dataState,
internalState: stateContainer.internalState, internalState: stateContainer.internalState,
runtimeStateManager: stateContainer.runtimeStateManager,
services: discoverServiceMock, services: discoverServiceMock,
setDataView: stateContainer.actions.setDataView, setDataView: stateContainer.actions.setDataView,
}); });

View file

@ -8,7 +8,7 @@
*/ */
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import type { DiscoverInternalStateContainer } from '../discover_internal_state_container'; import type { InternalStateStore, RuntimeStateManager } from '../redux';
import type { DiscoverServices } from '../../../../build_services'; import type { DiscoverServices } from '../../../../build_services';
import type { DiscoverSavedSearchContainer } from '../discover_saved_search_container'; import type { DiscoverSavedSearchContainer } from '../discover_saved_search_container';
import type { DiscoverDataStateContainer } from '../discover_data_state_container'; import type { DiscoverDataStateContainer } from '../discover_data_state_container';
@ -38,13 +38,15 @@ export const buildStateSubscribe =
appState, appState,
dataState, dataState,
internalState, internalState,
runtimeStateManager,
savedSearchState, savedSearchState,
services, services,
setDataView, setDataView,
}: { }: {
appState: DiscoverAppStateContainer; appState: DiscoverAppStateContainer;
dataState: DiscoverDataStateContainer; dataState: DiscoverDataStateContainer;
internalState: DiscoverInternalStateContainer; internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
savedSearchState: DiscoverSavedSearchContainer; savedSearchState: DiscoverSavedSearchContainer;
services: DiscoverServices; services: DiscoverServices;
setDataView: DiscoverStateContainer['actions']['setDataView']; setDataView: DiscoverStateContainer['actions']['setDataView'];
@ -106,7 +108,8 @@ export const buildStateSubscribe =
dataViewId, dataViewId,
savedSearch, savedSearch,
isEsqlMode, isEsqlMode,
internalStateContainer: internalState, internalState,
runtimeStateManager,
services, services,
}); });

View file

@ -16,72 +16,79 @@ import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../../__mocks__/services'; import { discoverServiceMock } from '../../../../__mocks__/services';
import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { PureTransitionsToTransitions } from '@kbn/kibana-utils-plugin/common/state_containers';
import { InternalStateTransitions } from '../discover_internal_state_container';
import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../../common/data_sources';
import { createRuntimeStateManager, internalStateActions } from '../redux';
const setupTestParams = (dataView: DataView | undefined) => { const setupTestParams = (dataView: DataView | undefined) => {
const savedSearch = savedSearchMock; const savedSearch = savedSearchMock;
const services = discoverServiceMock; const services = discoverServiceMock;
const runtimeStateManager = createRuntimeStateManager();
const discoverState = getDiscoverStateMock({ const discoverState = getDiscoverStateMock({ savedSearch, runtimeStateManager });
savedSearch, discoverState.internalState.dispatch(
}); internalStateActions.setDataView(savedSearch.searchSource.getField('index')!)
discoverState.internalState.transitions.setDataView(savedSearch.searchSource.getField('index')!); );
services.dataViews.get = jest.fn(() => Promise.resolve(dataView as DataView)); services.dataViews.get = jest.fn(() => Promise.resolve(dataView as DataView));
discoverState.appState.update = jest.fn(); discoverState.appState.update = jest.fn();
discoverState.internalState.transitions = {
setIsDataViewLoading: jest.fn(),
setResetDefaultProfileState: jest.fn(),
} as unknown as Readonly<PureTransitionsToTransitions<InternalStateTransitions>>;
return { return {
services, services,
appState: discoverState.appState, appState: discoverState.appState,
internalState: discoverState.internalState, internalState: discoverState.internalState,
runtimeStateManager,
}; };
}; };
describe('changeDataView', () => { 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 () => { 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 params = setupTestParams(dataViewWithDefaultColumnMock);
await changeDataView(dataViewWithDefaultColumnMock.id!, params); const promise = changeDataView({ dataViewId: dataViewWithDefaultColumnMock.id!, ...params });
expect(params.internalState.getState().isDataViewLoading).toBe(true);
await promise;
expect(params.appState.update).toHaveBeenCalledWith({ expect(params.appState.update).toHaveBeenCalledWith({
columns: ['default_column'], // default_column would be added as dataViewWithDefaultColumn has it as a mapped field 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' }), dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-user-default-column-id' }),
sort: [['@timestamp', 'desc']], sort: [['@timestamp', 'desc']],
}); });
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); expect(params.internalState.getState().isDataViewLoading).toBe(false);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
}); });
it('should set the right app state when a valid data view to switch to is given', async () => { it('should set the right app state when a valid data view to switch to is given', async () => {
const params = setupTestParams(dataViewComplexMock); const params = setupTestParams(dataViewComplexMock);
await changeDataView(dataViewComplexMock.id!, params); const promise = changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
expect(params.internalState.getState().isDataViewLoading).toBe(true);
await promise;
expect(params.appState.update).toHaveBeenCalledWith({ expect(params.appState.update).toHaveBeenCalledWith({
columns: [], // default_column would not be added as dataViewComplexMock does not have it as a mapped field 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' }), dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-various-field-types-id' }),
sort: [['data', 'desc']], sort: [['data', 'desc']],
}); });
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); expect(params.internalState.getState().isDataViewLoading).toBe(false);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
}); });
it('should not set the app state when an invalid data view to switch to is given', async () => { it('should not set the app state when an invalid data view to switch to is given', async () => {
const params = setupTestParams(undefined); const params = setupTestParams(undefined);
await changeDataView('data-view-with-various-field-types', params); const promise = changeDataView({ dataViewId: 'data-view-with-various-field-types', ...params });
expect(params.internalState.getState().isDataViewLoading).toBe(true);
await promise;
expect(params.appState.update).not.toHaveBeenCalled(); expect(params.appState.update).not.toHaveBeenCalled();
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); expect(params.internalState.getState().isDataViewLoading).toBe(false);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
}); });
it('should call setResetDefaultProfileState correctly when switching data view', async () => { it('should call setResetDefaultProfileState correctly when switching data view', async () => {
const params = setupTestParams(dataViewComplexMock); const params = setupTestParams(dataViewComplexMock);
expect(params.internalState.transitions.setResetDefaultProfileState).not.toHaveBeenCalled(); expect(params.internalState.getState().resetDefaultProfileState).toEqual(
await changeDataView(dataViewComplexMock.id!, params); expect.objectContaining({
expect(params.internalState.transitions.setResetDefaultProfileState).toHaveBeenCalledWith({ columns: false,
columns: true, rowHeight: false,
rowHeight: true, breakdownField: false,
breakdownField: true, })
}); );
await changeDataView({ dataViewId: dataViewComplexMock.id!, ...params });
expect(params.internalState.getState().resetDefaultProfileState).toEqual(
expect.objectContaining({
columns: true,
rowHeight: true,
breakdownField: true,
})
);
}); });
}); });

View file

@ -14,38 +14,40 @@ import {
SORT_DEFAULT_ORDER_SETTING, SORT_DEFAULT_ORDER_SETTING,
DEFAULT_COLUMNS_SETTING, DEFAULT_COLUMNS_SETTING,
} from '@kbn/discover-utils'; } from '@kbn/discover-utils';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverAppStateContainer } from '../discover_app_state_container'; import { DiscoverAppStateContainer } from '../discover_app_state_container';
import { addLog } from '../../../../utils/add_log'; import { addLog } from '../../../../utils/add_log';
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { getDataViewAppState } from './get_switch_data_view_app_state'; import { getDataViewAppState } from './get_switch_data_view_app_state';
import { internalStateActions, type InternalStateStore, type RuntimeStateManager } from '../redux';
/** /**
* Function executed when switching data view in the UI * Function executed when switching data view in the UI
*/ */
export async function changeDataView( export async function changeDataView({
id: string | DataView, dataViewId,
{ services,
services, internalState,
internalState, runtimeStateManager,
appState, appState,
}: { }: {
services: DiscoverServices; dataViewId: string | DataView;
internalState: DiscoverInternalStateContainer; services: DiscoverServices;
appState: DiscoverAppStateContainer; internalState: InternalStateStore;
} runtimeStateManager: RuntimeStateManager;
) { appState: DiscoverAppStateContainer;
addLog('[ui] changeDataView', { id }); }) {
addLog('[ui] changeDataView', { id: dataViewId });
const { dataViews, uiSettings } = services; const { dataViews, uiSettings } = services;
const dataView = internalState.getState().dataView; const currentDataView = runtimeStateManager.currentDataView$.getValue();
const state = appState.getState(); const state = appState.getState();
let nextDataView: DataView | null = null; let nextDataView: DataView | null = null;
internalState.transitions.setIsDataViewLoading(true); internalState.dispatch(internalStateActions.setIsDataViewLoading(true));
try { try {
nextDataView = typeof id === 'string' ? await dataViews.get(id, false) : id; nextDataView =
typeof dataViewId === 'string' ? await dataViews.get(dataViewId, false) : dataViewId;
// If nextDataView is an ad hoc data view with no fields, refresh its field list. // If nextDataView is an ad hoc data view with no fields, refresh its field list.
// This can happen when default profile data views are created without fields // This can happen when default profile data views are created without fields
@ -57,16 +59,18 @@ export async function changeDataView(
// Swallow the error and keep the current data view // Swallow the error and keep the current data view
} }
if (nextDataView && dataView) { if (nextDataView && currentDataView) {
// Reset the default profile state if we are switching to a different data view // Reset the default profile state if we are switching to a different data view
internalState.transitions.setResetDefaultProfileState({ internalState.dispatch(
columns: true, internalStateActions.setResetDefaultProfileState({
rowHeight: true, columns: true,
breakdownField: true, rowHeight: true,
}); breakdownField: true,
})
);
const nextAppState = getDataViewAppState( const nextAppState = getDataViewAppState(
dataView, currentDataView,
nextDataView, nextDataView,
uiSettings.get(DEFAULT_COLUMNS_SETTING, []), uiSettings.get(DEFAULT_COLUMNS_SETTING, []),
state.columns || [], state.columns || [],
@ -79,9 +83,9 @@ export async function changeDataView(
appState.update(nextAppState); appState.update(nextAppState);
if (internalState.getState().expandedDoc) { if (internalState.getState().expandedDoc) {
internalState.transitions.setExpandedDoc(undefined); internalState.dispatch(internalStateActions.setExpandedDoc(undefined));
} }
} }
internalState.transitions.setIsDataViewLoading(false); internalState.dispatch(internalStateActions.setIsDataViewLoading(false));
} }

View file

@ -16,8 +16,8 @@ import {
getMergedAccessor, getMergedAccessor,
ProfilesManager, ProfilesManager,
} from '../../../../context_awareness'; } from '../../../../context_awareness';
import type { InternalState } from '../discover_internal_state_container';
import type { DataDocumentsMsg } from '../discover_data_state_container'; import type { DataDocumentsMsg } from '../discover_data_state_container';
import type { DiscoverInternalState } from '../redux';
export const getDefaultProfileState = ({ export const getDefaultProfileState = ({
profilesManager, profilesManager,
@ -25,7 +25,7 @@ export const getDefaultProfileState = ({
dataView, dataView,
}: { }: {
profilesManager: ProfilesManager; profilesManager: ProfilesManager;
resetDefaultProfileState: InternalState['resetDefaultProfileState']; resetDefaultProfileState: DiscoverInternalState['resetDefaultProfileState'];
dataView: DataView; dataView: DataView;
}) => { }) => {
const defaultState = getDefaultState(profilesManager, dataView); const defaultState = getDefaultState(profilesManager, dataView);

View file

@ -12,7 +12,6 @@ import { cloneDeep, isEqual } from 'lodash';
import { isOfAggregateQueryType } from '@kbn/es-query'; import { isOfAggregateQueryType } from '@kbn/es-query';
import { getEsqlDataView } from './get_esql_data_view'; import { getEsqlDataView } from './get_esql_data_view';
import { loadAndResolveDataView } from './resolve_data_view'; import { loadAndResolveDataView } from './resolve_data_view';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverDataStateContainer } from '../discover_data_state_container'; import { DiscoverDataStateContainer } from '../discover_data_state_container';
import { cleanupUrlState } from './cleanup_url_state'; import { cleanupUrlState } from './cleanup_url_state';
import { getValidFilters } from '../../../../utils/get_valid_filters'; import { getValidFilters } from '../../../../utils/get_valid_filters';
@ -27,11 +26,13 @@ import {
import { DiscoverGlobalStateContainer } from '../discover_global_state_container'; import { DiscoverGlobalStateContainer } from '../discover_global_state_container';
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources'; import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
import { InternalStateStore, RuntimeStateManager, internalStateActions } from '../redux';
interface LoadSavedSearchDeps { interface LoadSavedSearchDeps {
appStateContainer: DiscoverAppStateContainer; appStateContainer: DiscoverAppStateContainer;
dataStateContainer: DiscoverDataStateContainer; dataStateContainer: DiscoverDataStateContainer;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
savedSearchContainer: DiscoverSavedSearchContainer; savedSearchContainer: DiscoverSavedSearchContainer;
globalStateContainer: DiscoverGlobalStateContainer; globalStateContainer: DiscoverGlobalStateContainer;
services: DiscoverServices; services: DiscoverServices;
@ -51,7 +52,8 @@ export const loadSavedSearch = async (
const { savedSearchId, initialAppState } = params ?? {}; const { savedSearchId, initialAppState } = params ?? {};
const { const {
appStateContainer, appStateContainer,
internalStateContainer, internalState,
runtimeStateManager,
savedSearchContainer, savedSearchContainer,
globalStateContainer, globalStateContainer,
services, services,
@ -75,7 +77,8 @@ export const loadSavedSearch = async (
dataViewId, dataViewId,
query: appState?.query, query: appState?.query,
services, services,
internalStateContainer, internalState,
runtimeStateManager,
}) })
); );
} }
@ -110,7 +113,8 @@ export const loadSavedSearch = async (
query: appState.query, query: appState.query,
savedSearch: nextSavedSearch, savedSearch: nextSavedSearch,
services, services,
internalStateContainer, internalState,
runtimeStateManager,
}); });
const dataViewDifferentToAppState = stateDataView.id !== savedSearchDataViewId; const dataViewDifferentToAppState = stateDataView.id !== savedSearchDataViewId;
if ( if (
@ -142,7 +146,7 @@ export const loadSavedSearch = async (
nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters(); nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters();
} }
internalStateContainer.transitions.resetOnSavedSearchChange(); internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
return nextSavedSearch; return nextSavedSearch;
}; };
@ -153,12 +157,12 @@ export const loadSavedSearch = async (
* @param deps * @param deps
*/ */
function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps) { function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps) {
const { dataStateContainer, internalStateContainer, services, setDataView } = deps; const { dataStateContainer, internalState, services, setDataView } = deps;
const savedSearchDataView = savedSearch.searchSource.getField('index')!; const savedSearchDataView = savedSearch.searchSource.getField('index')!;
setDataView(savedSearchDataView); setDataView(savedSearchDataView);
if (!savedSearchDataView.isPersisted()) { if (!savedSearchDataView.isPersisted()) {
internalStateContainer.transitions.appendAdHocDataViews(savedSearchDataView); internalState.dispatch(internalStateActions.appendAdHocDataViews(savedSearchDataView));
} }
// Finally notify dataStateContainer, data.query and filterManager about new derived state // Finally notify dataStateContainer, data.query and filterManager about new derived state
@ -196,13 +200,15 @@ const getStateDataView = async (
query, query,
savedSearch, savedSearch,
services, services,
internalStateContainer, internalState,
runtimeStateManager,
}: { }: {
dataViewId?: string; dataViewId?: string;
query: DiscoverAppState['query']; query: DiscoverAppState['query'];
savedSearch?: SavedSearch; savedSearch?: SavedSearch;
services: DiscoverServices; services: DiscoverServices;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
} }
) => { ) => {
const { dataView, dataViewSpec } = params; const { dataView, dataViewSpec } = params;
@ -222,7 +228,8 @@ const getStateDataView = async (
savedSearch, savedSearch,
isEsqlMode: isEsqlQuery, isEsqlMode: isEsqlQuery,
services, services,
internalStateContainer, internalState,
runtimeStateManager,
}); });
return result.dataView; return result.dataView;

View file

@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n';
import type { DataView, DataViewListItem, DataViewSpec } from '@kbn/data-views-plugin/public'; import type { DataView, DataViewListItem, DataViewSpec } from '@kbn/data-views-plugin/public';
import type { ToastsStart } from '@kbn/core/public'; import type { ToastsStart } from '@kbn/core/public';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { InternalStateStore, RuntimeStateManager } from '../redux';
interface DataViewData { interface DataViewData {
/** /**
@ -174,18 +174,21 @@ export const loadAndResolveDataView = async ({
dataViewSpec, dataViewSpec,
savedSearch, savedSearch,
isEsqlMode, isEsqlMode,
internalStateContainer, internalState,
runtimeStateManager,
services, services,
}: { }: {
dataViewId?: string; dataViewId?: string;
dataViewSpec?: DataViewSpec; dataViewSpec?: DataViewSpec;
savedSearch?: SavedSearch; savedSearch?: SavedSearch;
isEsqlMode?: boolean; isEsqlMode?: boolean;
internalStateContainer: DiscoverInternalStateContainer; internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
services: DiscoverServices; services: DiscoverServices;
}) => { }) => {
const { dataViews, toastNotifications } = services; const { dataViews, toastNotifications } = services;
const { adHocDataViews, savedDataViews } = internalStateContainer.getState(); const adHocDataViews = runtimeStateManager.adHocDataViews$.getValue();
const { savedDataViews } = internalState.getState();
// Check ad hoc data views first, unless a data view spec is supplied, // Check ad hoc data views first, unless a data view spec is supplied,
// then attempt to load one if none is found // then attempt to load one if none is found

View file

@ -11,9 +11,11 @@ import { renderHook } from '@testing-library/react';
import { useDefaultAdHocDataViews } from './use_default_ad_hoc_data_views'; import { useDefaultAdHocDataViews } from './use_default_ad_hoc_data_views';
import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
import { discoverServiceMock } from '../../__mocks__/services'; import { discoverServiceMock } from '../../__mocks__/services';
import { DataView } from '@kbn/data-views-plugin/common';
import React from 'react'; import React from 'react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { internalStateActions } from '../../application/main/state_management/redux';
import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { omit } from 'lodash';
const renderDefaultAdHocDataViewsHook = ({ const renderDefaultAdHocDataViewsHook = ({
rootProfileLoading, rootProfileLoading,
@ -23,20 +25,22 @@ const renderDefaultAdHocDataViewsHook = ({
const clearInstanceCache = jest.spyOn(discoverServiceMock.dataViews, 'clearInstanceCache'); const clearInstanceCache = jest.spyOn(discoverServiceMock.dataViews, 'clearInstanceCache');
const createDataView = jest const createDataView = jest
.spyOn(discoverServiceMock.dataViews, 'create') .spyOn(discoverServiceMock.dataViews, 'create')
.mockImplementation((spec) => Promise.resolve(spec as unknown as DataView)); .mockImplementation((spec) => Promise.resolve(buildDataViewMock(omit(spec, 'fields'))));
const existingAdHocDataVew = { id: '1', title: 'test' } as unknown as DataView; const existingAdHocDataVew = buildDataViewMock({ id: '1', title: 'test' });
const previousSpecs = [ const previousDataViews = [
{ id: '2', title: 'tes2' }, buildDataViewMock({ id: '2', title: 'tes2' }),
{ id: '3', title: 'test3' }, buildDataViewMock({ id: '3', title: 'test3' }),
]; ];
const newSpecs = [ const newDataViews = [
{ id: '4', title: 'test4' }, buildDataViewMock({ id: '4', title: 'test4' }),
{ id: '5', title: 'test5' }, buildDataViewMock({ id: '5', title: 'test5' }),
]; ];
const stateContainer = getDiscoverStateMock({}); const stateContainer = getDiscoverStateMock({});
stateContainer.internalState.transitions.appendAdHocDataViews(existingAdHocDataVew); stateContainer.internalState.dispatch(
stateContainer.internalState.transitions.setDefaultProfileAdHocDataViews( internalStateActions.appendAdHocDataViews(existingAdHocDataVew)
previousSpecs as unknown as DataView[] );
stateContainer.internalState.dispatch(
internalStateActions.setDefaultProfileAdHocDataViews(previousDataViews)
); );
const { result, unmount } = renderHook(useDefaultAdHocDataViews, { const { result, unmount } = renderHook(useDefaultAdHocDataViews, {
initialProps: { initialProps: {
@ -44,7 +48,11 @@ const renderDefaultAdHocDataViewsHook = ({
rootProfileState: { rootProfileState: {
rootProfileLoading, rootProfileLoading,
AppWrapper: () => null, AppWrapper: () => null,
getDefaultAdHocDataViews: () => newSpecs, getDefaultAdHocDataViews: () =>
newDataViews.map((dv) => {
const { id, ...restSpec } = dv.toSpec();
return { id: id!, ...restSpec };
}),
}, },
}, },
wrapper: ({ children }) => ( wrapper: ({ children }) => (
@ -58,8 +66,8 @@ const renderDefaultAdHocDataViewsHook = ({
createDataView, createDataView,
stateContainer, stateContainer,
existingAdHocDataVew, existingAdHocDataVew,
previousSpecs, previousDataViews,
newSpecs, newDataViews,
}; };
}; };
@ -75,27 +83,26 @@ describe('useDefaultAdHocDataViews', () => {
createDataView, createDataView,
stateContainer, stateContainer,
existingAdHocDataVew, existingAdHocDataVew,
previousSpecs, previousDataViews,
newSpecs, newDataViews,
} = renderDefaultAdHocDataViewsHook({ rootProfileLoading: false }); } = renderDefaultAdHocDataViewsHook({ rootProfileLoading: false });
expect(clearInstanceCache).not.toHaveBeenCalled(); expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled(); expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([ expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew, existingAdHocDataVew,
...previousSpecs, ...previousDataViews,
]); ]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual( expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id) previousDataViews.map((dv) => dv.id)
); );
await result.current.initializeProfileDataViews(); await result.current.initializeProfileDataViews();
expect(clearInstanceCache.mock.calls).toEqual(previousSpecs.map((s) => [s.id])); expect(clearInstanceCache.mock.calls).toEqual(previousDataViews.map((dv) => [dv.id]));
expect(createDataView.mock.calls).toEqual(newSpecs.map((s) => [s, true])); expect(createDataView.mock.calls).toEqual(newDataViews.map((dv) => [dv.toSpec(), true]));
expect(stateContainer.internalState.get().adHocDataViews).toEqual([ expect(
existingAdHocDataVew, stateContainer.runtimeStateManager.adHocDataViews$.getValue().map((dv) => dv.id)
...newSpecs, ).toEqual([existingAdHocDataVew.id, ...newDataViews.map((dv) => dv.id)]);
]); expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual( newDataViews.map((dv) => dv.id)
newSpecs.map((s) => s.id)
); );
}); });
@ -106,43 +113,45 @@ describe('useDefaultAdHocDataViews', () => {
createDataView, createDataView,
stateContainer, stateContainer,
existingAdHocDataVew, existingAdHocDataVew,
previousSpecs, previousDataViews,
} = renderDefaultAdHocDataViewsHook({ rootProfileLoading: true }); } = renderDefaultAdHocDataViewsHook({ rootProfileLoading: true });
expect(clearInstanceCache).not.toHaveBeenCalled(); expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled(); expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([ expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew, existingAdHocDataVew,
...previousSpecs, ...previousDataViews,
]); ]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual( expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id) previousDataViews.map((dv) => dv.id)
); );
await result.current.initializeProfileDataViews(); await result.current.initializeProfileDataViews();
expect(clearInstanceCache).not.toHaveBeenCalled(); expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled(); expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([ expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew, existingAdHocDataVew,
...previousSpecs, ...previousDataViews,
]); ]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual( expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id) previousDataViews.map((dv) => dv.id)
); );
}); });
it('should clear instance cache on unmount', async () => { it('should clear instance cache on unmount', async () => {
const { unmount, clearInstanceCache, stateContainer, existingAdHocDataVew, previousSpecs } = const { unmount, clearInstanceCache, stateContainer, existingAdHocDataVew, previousDataViews } =
renderDefaultAdHocDataViewsHook({ rootProfileLoading: false }); renderDefaultAdHocDataViewsHook({ rootProfileLoading: false });
expect(clearInstanceCache).not.toHaveBeenCalled(); expect(clearInstanceCache).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([ expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew, existingAdHocDataVew,
...previousSpecs, ...previousDataViews,
]); ]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual( expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id) previousDataViews.map((dv) => dv.id)
); );
unmount(); unmount();
expect(clearInstanceCache.mock.calls).toEqual(previousSpecs.map((s) => [s.id])); expect(clearInstanceCache.mock.calls).toEqual(previousDataViews.map((s) => [s.id]));
expect(stateContainer.internalState.get().adHocDataViews).toEqual([existingAdHocDataVew]); expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual([]); existingAdHocDataVew,
]);
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual([]);
}); });
}); });

View file

@ -13,6 +13,7 @@ import useUnmount from 'react-use/lib/useUnmount';
import type { RootProfileState } from './use_root_profile'; import type { RootProfileState } from './use_root_profile';
import { useDiscoverServices } from '../../hooks/use_discover_services'; import { useDiscoverServices } from '../../hooks/use_discover_services';
import type { DiscoverStateContainer } from '../../application/main/state_management/discover_state'; import type { DiscoverStateContainer } from '../../application/main/state_management/discover_state';
import { internalStateActions } from '../../application/main/state_management/redux';
/** /**
* Hook to retrieve and initialize the default profile ad hoc data views * Hook to retrieve and initialize the default profile ad hoc data views
@ -36,7 +37,7 @@ export const useDefaultAdHocDataViews = ({
// Clear the cache of old data views before creating // Clear the cache of old data views before creating
// the new ones to avoid cache hits on duplicate IDs // the new ones to avoid cache hits on duplicate IDs
for (const prevId of internalState.get().defaultProfileAdHocDataViewIds) { for (const prevId of internalState.getState().defaultProfileAdHocDataViewIds) {
dataViews.clearInstanceCache(prevId); dataViews.clearInstanceCache(prevId);
} }
@ -45,7 +46,7 @@ export const useDefaultAdHocDataViews = ({
profileDataViewSpecs.map((spec) => dataViews.create(spec, true)) profileDataViewSpecs.map((spec) => dataViews.create(spec, true))
); );
internalState.transitions.setDefaultProfileAdHocDataViews(profileDataViews); internalState.dispatch(internalStateActions.setDefaultProfileAdHocDataViews(profileDataViews));
}); });
// This approach allows us to return a callback with a stable reference // This approach allows us to return a callback with a stable reference
@ -53,11 +54,11 @@ export const useDefaultAdHocDataViews = ({
// Make sure to clean up on unmount // Make sure to clean up on unmount
useUnmount(() => { useUnmount(() => {
for (const prevId of internalState.get().defaultProfileAdHocDataViewIds) { for (const prevId of internalState.getState().defaultProfileAdHocDataViewIds) {
dataViews.clearInstanceCache(prevId); dataViews.clearInstanceCache(prevId);
} }
internalState.transitions.setDefaultProfileAdHocDataViews([]); internalState.dispatch(internalStateActions.setDefaultProfileAdHocDataViews([]));
}); });
return { initializeProfileDataViews }; return { initializeProfileDataViews };

View file

@ -40,17 +40,14 @@ describe('useDiscoverCustomizationService', () => {
customizationCallbacks: [callback], customizationCallbacks: [callback],
}) })
); );
expect(wrapper.result.current.isInitialized).toBe(false); expect(wrapper.result.current).toBeUndefined();
expect(wrapper.result.current.customizationService).toBeUndefined();
expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1);
const cleanup = jest.fn(); const cleanup = jest.fn();
await act(async () => { await act(async () => {
resolveCallback(cleanup); resolveCallback(cleanup);
await promise; await promise;
}); });
expect(wrapper.result.current.isInitialized).toBe(true); expect(wrapper.result.current).toBe(service);
expect(wrapper.result.current.customizationService).toBeDefined();
expect(wrapper.result.current.customizationService).toBe(service);
expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1);
expect(cleanup).not.toHaveBeenCalled(); expect(cleanup).not.toHaveBeenCalled();
wrapper.unmount(); wrapper.unmount();

View file

@ -50,9 +50,7 @@ export const useDiscoverCustomizationService = ({
}; };
}); });
const isInitialized = Boolean(customizationService); return customizationService;
return { customizationService, isInitialized };
}; };
export const useDiscoverCustomization$ = <TCustomizationId extends DiscoverCustomizationId>( export const useDiscoverCustomization$ = <TCustomizationId extends DiscoverCustomizationId>(

View file

@ -100,7 +100,8 @@
"@kbn/esql-ast", "@kbn/esql-ast",
"@kbn/discover-shared-plugin", "@kbn/discover-shared-plugin",
"@kbn/response-ops-rule-form", "@kbn/response-ops-rule-form",
"@kbn/embeddable-enhanced-plugin" "@kbn/embeddable-enhanced-plugin",
"@kbn/shared-ux-page-analytics-no-data-types"
], ],
"exclude": ["target/**/*"] "exclude": ["target/**/*"]
} }

View file

@ -20,7 +20,6 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects'); const testSubjects = getService('testSubjects');
const browser = getService('browser'); const browser = getService('browser');
const dataGrid = getService('dataGrid');
const retry = getService('retry'); const retry = getService('retry');
const defaultSettings = { defaultIndex: 'logstash-*' }; const defaultSettings = { defaultIndex: 'logstash-*' };
@ -69,33 +68,5 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await browser.goBack(); await browser.goBack();
await header.waitUntilLoadingHasFinished(); await header.waitUntilLoadingHasFinished();
}); });
it('Search bar Prepend Filters exists and should apply filter properly', async () => {
// Validate custom filters are present
await testSubjects.existOrFail('customPrependedFilter');
await testSubjects.click('customPrependedFilter');
await testSubjects.existOrFail('optionsList-control-selection-exists');
// Retrieve option list popover
const optionsListControl = await testSubjects.find('optionsList-control-popover');
const optionsItems = await optionsListControl.findAllByCssSelector(
'[data-test-subj*="optionsList-control-selection-"]'
);
// Retrieve second item in the options along with the count of documents
const item = optionsItems[1];
const countBadge = await item.findByCssSelector(
'[data-test-subj="optionsList-document-count-badge"]'
);
const documentsCount = parseInt(await countBadge.getVisibleText(), 10);
// Click the item to apply filter
await item.click();
await header.waitUntilLoadingHasFinished();
// Validate that filter is applied
const rows = await dataGrid.getDocTableRows();
await expect(documentsCount).to.eql(rows.length);
});
}); });
}; };

View file

@ -6,12 +6,12 @@
*/ */
import type { DiscoverAppState } from '@kbn/discover-plugin/public/application/main/state_management/discover_app_state_container'; import type { DiscoverAppState } from '@kbn/discover-plugin/public/application/main/state_management/discover_app_state_container';
import type { InternalState } from '@kbn/discover-plugin/public/application/main/state_management/discover_internal_state_container'; import type { DiscoverInternalState } from '@kbn/discover-plugin/public/application/main/state_management/redux';
import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import type { SavedSearch } from '@kbn/saved-search-plugin/common';
export interface SecuritySolutionDiscoverState { export interface SecuritySolutionDiscoverState {
app: DiscoverAppState | undefined; app: DiscoverAppState | undefined;
internal: InternalState | undefined; internal: DiscoverInternalState | undefined;
savedSearch: SavedSearch | undefined; savedSearch: SavedSearch | undefined;
} }

View file

@ -11,7 +11,7 @@ import { useHistory } from 'react-router-dom';
import type { CustomizationCallback } from '@kbn/discover-plugin/public/customizations/types'; import type { CustomizationCallback } from '@kbn/discover-plugin/public/customizations/types';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
import type { ScopedHistory } from '@kbn/core/public'; import type { ScopedHistory } from '@kbn/core/public';
import type { Subscription } from 'rxjs'; import { from, type Subscription } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { isEqualWith } from 'lodash'; import { isEqualWith } from 'lodash';
@ -219,7 +219,7 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
next: setDiscoverAppState, next: setDiscoverAppState,
}); });
const internalStateSubscription = stateContainer.internalState.state$.subscribe({ const internalStateSubscription = from(stateContainer.internalState).subscribe({
next: setDiscoverInternalState, next: setDiscoverInternalState,
}); });

View file

@ -6,7 +6,7 @@
*/ */
import type { DiscoverAppState } from '@kbn/discover-plugin/public/application/main/state_management/discover_app_state_container'; import type { DiscoverAppState } from '@kbn/discover-plugin/public/application/main/state_management/discover_app_state_container';
import type { InternalState } from '@kbn/discover-plugin/public/application/main/state_management/discover_internal_state_container'; import type { DiscoverInternalState } from '@kbn/discover-plugin/public/application/main/state_management/redux';
import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import type { SavedSearch } from '@kbn/saved-search-plugin/common';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -22,7 +22,7 @@ export const useDiscoverState = () => {
const result = state.discover.app; const result = state.discover.app;
return result; return result;
}); });
const discoverInternalState = useSelector<State, InternalState | undefined>((state) => { const discoverInternalState = useSelector<State, DiscoverInternalState | undefined>((state) => {
const result = state.discover.internal; const result = state.discover.internal;
return result; return result;
}); });
@ -41,7 +41,7 @@ export const useDiscoverState = () => {
); );
const setDiscoverInternalState = useCallback( const setDiscoverInternalState = useCallback(
(newState: InternalState) => { (newState: DiscoverInternalState) => {
dispatch(updateDiscoverInternalState({ newState })); dispatch(updateDiscoverInternalState({ newState }));
}, },
[dispatch] [dispatch]

View file

@ -17,7 +17,6 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects'); const testSubjects = getService('testSubjects');
const browser = getService('browser'); const browser = getService('browser');
const dataGrid = getService('dataGrid');
const retry = getService('retry'); const retry = getService('retry');
const defaultSettings = { defaultIndex: 'logstash-*' }; const defaultSettings = { defaultIndex: 'logstash-*' };
@ -67,33 +66,5 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await browser.goBack(); await browser.goBack();
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
}); });
it('Search bar Prepend Filters exists and should apply filter properly', async () => {
// Validate custom filters are present
await testSubjects.existOrFail('customPrependedFilter');
await testSubjects.click('customPrependedFilter');
await testSubjects.existOrFail('optionsList-control-selection-exists');
// Retrieve option list popover
const optionsListControl = await testSubjects.find('optionsList-control-popover');
const optionsItems = await optionsListControl.findAllByCssSelector(
'[data-test-subj*="optionsList-control-selection-"]'
);
// Retrieve second item in the options along with the count of documents
const item = optionsItems[1];
const countBadge = await item.findByCssSelector(
'[data-test-subj="optionsList-document-count-badge"]'
);
const documentsCount = parseInt(await countBadge.getVisibleText(), 10);
// Click the item to apply filter
await item.click();
await PageObjects.header.waitUntilLoadingHasFinished();
// Validate that filter is applied
const rows = await dataGrid.getDocTableRows();
await expect(documentsCount).to.eql(rows.length);
});
}); });
}; };