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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../.
import { createCustomizationService } from '../../../../customizations/customization_service';
import { DiscoverGrid } from '../../../../components/discover_grid';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import { internalStateActions } from '../../state_management/redux';
const customisationService = createCustomizationService();
@ -46,16 +47,18 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
stateContainer.appState.update({
dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
});
stateContainer.internalState.transitions.setDataRequestParams({
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
});
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
})
);
stateContainer.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 { DiscoverGrid } from '../../../../components/discover_grid';
import { getDefaultRowsPerPage } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { useAppStateSelector } from '../../state_management/discover_app_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { FetchStatus } from '../../../types';
@ -73,6 +72,11 @@ import {
useAdditionalCellActions,
useProfileAccessor,
} from '../../../../context_awareness';
import {
internalStateActions,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
const containerStyles = css`
position: relative;
@ -108,6 +112,7 @@ function DiscoverDocumentsComponent({
onFieldEdited?: () => void;
}) {
const services = useDiscoverServices();
const dispatch = useInternalStateDispatch();
const documents$ = stateContainer.dataState.data$.documents$;
const savedSearch = useSavedSearchInitial();
const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services;
@ -204,9 +209,9 @@ function DiscoverDocumentsComponent({
const setExpandedDoc = useCallback(
(doc: DataTableRecord | undefined) => {
stateContainer.internalState.transitions.setExpandedDoc(doc);
dispatch(internalStateActions.setExpandedDoc(doc));
},
[stateContainer]
[dispatch]
);
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 { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
function getStateContainer(savedSearch?: SavedSearch) {
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
@ -46,17 +47,19 @@ function getStateContainer(savedSearch?: SavedSearch) {
stateContainer.appState.update(appState);
stateContainer.internalState.transitions.setDataView(dataView);
stateContainer.internalState.transitions.setDataRequestParams({
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
});
stateContainer.internalState.dispatch(internalStateActions.setDataView(dataView));
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
timeRangeRelative: {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
},
})
);
return stateContainer;
}
@ -142,7 +145,9 @@ const mountComponent = async ({
<KibanaRenderContextProvider {...services.core}>
<KibanaContextProvider services={services}>
<DiscoverMainProvider value={stateContainer}>
<DiscoverHistogramLayout {...props} />
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<DiscoverHistogramLayout {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>

View file

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

View file

@ -34,7 +34,6 @@ import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
import { DiscoverStateContainer } from '../../state_management/discover_state';
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 { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverNoResults } from '../no_results';
@ -54,6 +53,7 @@ import { DiscoverResizableLayout } from './discover_resizable_layout';
import { PanelsToggle, PanelsToggleProps } from '../../../../components/panels_toggle';
import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useCurrentDataView, useInternalStateSelector } from '../../state_management/redux';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav);
@ -89,7 +89,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
state.grid,
]);
const isEsqlMode = useIsEsqlMode();
const viewMode: VIEW_MODE = useAppStateSelector((state) => {
const fieldStatsNotAvailable =
!uiSettings.get(SHOW_FIELD_STATISTICS) && !!dataVisualizerService;
@ -98,15 +97,10 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
}
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
});
const [dataView, dataViewLoading] = useInternalStateSelector((state) => [
state.dataView!,
state.isDataViewLoading,
]);
const customFilters = useInternalStateSelector((state) => state.customFilters);
const dataView = useCurrentDataView();
const dataViewLoading = useInternalStateSelector((state) => state.isDataViewLoading);
const dataState: DataMainMsg = useDataState(main$);
const savedSearch = useSavedSearchInitial();
const fetchCounter = useRef<number>(0);
useEffect(() => {
@ -197,21 +191,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
[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>(
(field, values, operation) => {
if (!field || !isOfAggregateQueryType(query)) {
@ -430,7 +409,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sidebarToggleState$={sidebarToggleState$}
sidebarPanel={
<SidebarMemoized
additionalFilters={customFilters}
columns={currentColumns}
documents$={stateContainer.dataState.data$.documents$}
onAddBreakdownField={canSetBreakdownField ? onAddBreakdownField : undefined}
@ -503,3 +481,18 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
</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 { useDiscoverCustomization } from '../../../../customizations';
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();
let mockQueryState = {
@ -121,7 +123,11 @@ describe('useDiscoverHistogram', () => {
};
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(
@ -379,15 +385,17 @@ describe('useDiscoverHistogram', () => {
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 stateContainer = getStateContainer();
const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
const timeRangeRel = { from: 'now-15m', to: 'now' };
stateContainer.internalState.transitions.setDataRequestParams({
timeRangeAbsolute: timeRangeAbs,
timeRangeRelative: timeRangeRel,
});
stateContainer.internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: timeRangeAbs,
timeRangeRelative: timeRangeRel,
})
);
const { hook } = await renderUseDiscoverHistogram({ stateContainer });
act(() => {
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 type { DiscoverStateContainer } from '../../state_management/discover_state';
import { addLog } from '../../../../utils/add_log';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import {
useAppStateSelector,
type DiscoverAppState,
@ -52,6 +51,12 @@ import {
import { DataDocumentsMsg } from '../../state_management/discover_data_state_container';
import { useSavedSearch } from '../../state_management/discover_state_provider';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import {
internalStateActions,
useCurrentDataView,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
const EMPTY_ESQL_COLUMNS: DatatableColumn[] = [];
const EMPTY_FILTERS: Filter[] = [];
@ -220,7 +225,6 @@ export const useDiscoverHistogram = ({
*/
const { query, filters } = useQuerySubscriber({ data: services.data });
const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
const customFilters = useInternalStateSelector((state) => state.customFilters);
const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams;
// When in ES|QL mode, update the data view, query, and
// columns only when documents are done fetching so the Lens suggestions
@ -308,17 +312,18 @@ export const useDiscoverHistogram = ({
};
}, [isEsqlMode, stateContainer.dataState.fetchChart$, esqlFetchComplete$, unifiedHistogram]);
const dataView = useInternalStateSelector((state) => state.dataView!);
const dataView = useCurrentDataView();
const histogramCustomization = useDiscoverCustomization('unified_histogram');
const filtersMemoized = useMemo(() => {
const allFilters = [...(filters ?? []), ...customFilters];
const allFilters = [...(filters ?? [])];
return allFilters.length ? allFilters : EMPTY_FILTERS;
}, [filters, customFilters]);
}, [filters]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const timeRangeMemoized = useMemo(() => timeRange, [timeRange?.from, timeRange?.to]);
const dispatch = useInternalStateDispatch();
const onVisContextChanged = useCallback(
(
@ -332,31 +337,25 @@ export const useDiscoverHistogram = ({
stateContainer.savedSearchState.updateVisContext({
nextVisContext,
});
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation(
undefined
);
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
break;
case UnifiedHistogramExternalVisContextStatus.automaticallyOverridden:
// if the visualization was invalidated as incompatible and rebuilt
// (it will be used later for saving the visualization via Save button)
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation(
nextVisContext
);
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(nextVisContext));
break;
case UnifiedHistogramExternalVisContextStatus.automaticallyCreated:
case UnifiedHistogramExternalVisContextStatus.applied:
// clearing the value in the internal state so we don't use it during saved search saving
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation(
undefined
);
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation(undefined));
break;
case UnifiedHistogramExternalVisContextStatus.unknown:
// using `{}` to overwrite the value inside the saved search SO during saving
stateContainer.internalState.transitions.setOverriddenVisContextAfterInvalidation({});
dispatch(internalStateActions.setOverriddenVisContextAfterInvalidation({}));
break;
}
},
[stateContainer]
[dispatch, stateContainer.savedSearchState]
);
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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
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 { resetExistingFieldsCache } from '@kbn/unified-field-list/src/hooks/use_existing_fields';
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 { DiscoverCustomizationId } from '../../../../customizations/customization_service';
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 = {
id: 'search_bar',
@ -193,7 +193,7 @@ async function mountComponent(
services?: DiscoverServices
): Promise<ReactWrapper<DiscoverSidebarResponsiveProps>> {
let comp: ReactWrapper<DiscoverSidebarResponsiveProps>;
const { appState, internalState } = getStateContainer(appStateParams);
const stateContainer = getStateContainer(appStateParams);
const mockedServices = services ?? createMockServices();
mockedServices.data.dataViews.getIdsWithTitle = jest.fn(async () =>
props.selectedDataView
@ -203,16 +203,18 @@ async function mountComponent(
mockedServices.data.dataViews.get = jest.fn().mockImplementation(async (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 () => {
comp = mountWithIntl(
<KibanaContextProvider services={mockedServices}>
<DiscoverAppStateProvider value={appState}>
<InternalStateProvider value={internalState}>
<DiscoverSidebarResponsive {...props} />
</InternalStateProvider>
</DiscoverAppStateProvider>
<DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={props.selectedDataView!} adHocDataViews={[]}>
<DiscoverSidebarResponsive {...props} />{' '}
</RuntimeStateProvider>
</DiscoverMainProvider>
</KibanaContextProvider>
);
// wait for lazy modules

View file

@ -37,10 +37,7 @@ import {
import { useDiscoverCustomization } from '../../../../customizations';
import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import {
selectDataViewsForPicker,
useInternalStateSelector,
} from '../../state_management/discover_internal_state_container';
import { useDataViewsForPicker } from '../../state_management/redux';
const EMPTY_FIELD_COUNTS = {};
@ -176,8 +173,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
);
const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView);
const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL;
const { savedDataViews, managedDataViews, adHocDataViews } =
useInternalStateSelector(selectDataViewsForPicker);
const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker();
useEffect(() => {
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 { useDiscoverCustomization } from '../../../../customizations';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { RuntimeStateProvider, internalStateActions } from '../../state_management/redux';
jest.mock('@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;
}
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.transitions.setDataView(dataViewMock);
stateContainer.internalState.dispatch(internalStateActions.setDataView(dataViewMock));
return {
stateContainer,
@ -110,7 +111,9 @@ describe('Discover topnav component', () => {
const props = getProps({ capabilities: { discover_v2: { save: true } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);
const topNavMenu = component.find(TopNavMenu);
@ -122,7 +125,9 @@ describe('Discover topnav component', () => {
const props = getProps({ capabilities: { discover_v2: { save: false } } });
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);
const topNavMenu = component.find(TopNavMenu).props();
@ -144,7 +149,9 @@ describe('Discover topnav component', () => {
const props = getProps();
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);
const topNavMenu = component.find(TopNavMenu);
@ -164,7 +171,9 @@ describe('Discover topnav component', () => {
const props = getProps();
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);
@ -176,7 +185,9 @@ describe('Discover topnav component', () => {
const props = getProps();
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);
const topNav = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu);
@ -197,7 +208,9 @@ describe('Discover topnav component', () => {
const props = getProps();
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverTopNav {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
);

View file

@ -13,10 +13,6 @@ import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { DiscoverFlyouts, dismissAllFlyoutsExceptFor } from '@kbn/discover-utils';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
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 type { DiscoverStateContainer } from '../../state_management/discover_state';
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 { ESQLToDataViewTransitionModal } from './esql_dataview_transition';
import './top_nav.scss';
import {
internalStateActions,
useCurrentDataView,
useDataViewsForPicker,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
export interface DiscoverTopNavProps {
savedQuery?: string;
@ -46,12 +49,12 @@ export const DiscoverTopNav = ({
isLoading,
onCancelClick,
}: DiscoverTopNavProps) => {
const dispatch = useInternalStateDispatch();
const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data, setHeaderActionMenu } = services;
const query = useAppStateSelector((state) => state.query);
const { savedDataViews, managedDataViews, adHocDataViews } =
useInternalStateSelector(selectDataViewsForPicker);
const dataView = useInternalStateSelector((state) => state.dataView!);
const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker();
const dataView = useCurrentDataView();
const isESQLToDataViewTransitionModalVisible = useInternalStateSelector(
(state) => state.isESQLToDataViewTransitionModalVisible
);
@ -134,7 +137,7 @@ export const DiscoverTopNav = ({
if (shouldDismissModal) {
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
if (needsSave == null) {
return;
@ -145,9 +148,7 @@ export const DiscoverTopNav = ({
services,
state: stateContainer,
onClose: () =>
stateContainer.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(
false
),
dispatch(internalStateActions.setIsESQLToDataViewTransitionModalVisible(false)),
onSaveCb: () => {
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
},
@ -156,7 +157,7 @@ export const DiscoverTopNav = ({
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
}
},
[dataView.id, services, stateContainer]
[dataView.id, dispatch, services, 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 { createBrowserHistory } from 'history';
import { mockCustomizationContext } from '../../../../customizations/__mocks__/customization_context';
import { createRuntimeStateManager } from '../../state_management/redux';
function getStateContainer({ dataView }: { dataView?: DataView } = {}) {
const savedSearch = savedSearchMock;
@ -28,13 +29,14 @@ function getStateContainer({ dataView }: { dataView?: DataView } = {}) {
services: discoverServiceMock,
history,
customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
});
stateContainer.savedSearchState.set(savedSearch);
stateContainer.appState.getState = jest.fn(() => ({
rowsPerPage: 250,
}));
if (dataView) {
stateContainer.internalState.transitions.setDataView(dataView);
stateContainer.actions.setDataView(dataView);
}
return stateContainer;
}

View file

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

View file

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

View file

@ -15,6 +15,7 @@ import { useTopNavLinks } from './use_top_nav_links';
import { DiscoverServices } from '../../../../build_services';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
describe('useTopNavLinks', () => {
const services = {
@ -33,7 +34,11 @@ describe('useTopNavLinks', () => {
state.actions.setDataView(dataViewMock);
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', () => {

View file

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

View file

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

View file

@ -42,12 +42,12 @@ import {
} from '../state_management/discover_data_state_container';
import { DiscoverServices } from '../../../build_services';
import { fetchEsql } from './fetch_esql';
import { InternalState } from '../state_management/discover_internal_state_container';
import { InternalStateStore } from '../state_management/redux';
export interface FetchDeps {
abortController: AbortController;
getAppState: () => DiscoverAppState;
getInternalState: () => InternalState;
internalState: InternalStateStore;
initialFetchStatus: FetchStatus;
inspectorAdapters: Adapters;
savedSearch: SavedSearch;
@ -71,7 +71,7 @@ export function fetchAll(
const {
initialFetchStatus,
getAppState,
getInternalState,
internalState,
services,
inspectorAdapters,
savedSearch,
@ -96,8 +96,7 @@ export function fetchAll(
dataView,
services,
sort: getAppState().sort as SortOrder[],
customFilters: getInternalState().customFilters,
inputTimeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
inputTimeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
});
}
@ -118,7 +117,7 @@ export function fetchAll(
data,
expressions,
profilesManager,
timeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
timeRange: internalState.getState().dataRequestParams.timeRangeAbsolute,
})
: fetchDocuments(searchSource, fetchDeps);
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
@ -221,7 +220,7 @@ export async function fetchMoreDocuments(
fetchDeps: FetchDeps
): Promise<void> {
try {
const { getAppState, getInternalState, services, savedSearch } = fetchDeps;
const { getAppState, services, savedSearch } = fetchDeps;
const searchSource = savedSearch.searchSource.createChild();
const dataView = searchSource.getField('index')!;
const query = getAppState().query;
@ -249,7 +248,6 @@ export async function fetchMoreDocuments(
dataView,
services,
sort: getAppState().sort as SortOrder[],
customFilters: getInternalState().customFilters,
});
// 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 type { SortOrder } from '@kbn/saved-search-plugin/public';
import { discoverServiceMock } from '../../../__mocks__/services';
import { Filter } from '@kbn/es-query';
describe('updateVolatileSearchSource', () => {
test('updates a given search source', async () => {
@ -22,24 +21,9 @@ describe('updateVolatileSearchSource', () => {
dataView: dataViewMock,
services: discoverServiceMock,
sort: [] as SortOrder[],
customFilters: [],
});
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: true }]);
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 { 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 { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import { DiscoverServices } from '../../../build_services';
@ -24,13 +24,11 @@ export function updateVolatileSearchSource(
dataView,
services,
sort,
customFilters,
inputTimeRange,
}: {
dataView: DataView;
services: DiscoverServices;
sort?: SortOrder[];
customFilters: Filter[];
inputTimeRange?: TimeRange;
}
) {
@ -46,16 +44,14 @@ export function updateVolatileSearchSource(
searchSource.setField('trackTotalHits', true);
let filters = [...customFilters];
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
const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange);
filters = timeFilter ? [...filters, timeFilter] : filters;
searchSource.setField(
'filter',
data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange)
);
}
searchSource.setField('filter', filters);
searchSource.removeField('fieldsFromSource');
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 { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
import { DiscoverMainProvider } from './state_management/discover_state_provider';
import { RuntimeStateProvider, internalStateActions } from './state_management/redux';
discoverServiceMock.data.query.timefilter.timefilter.getTime = () => {
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
@ -32,7 +33,7 @@ describe('DiscoverMainApp', () => {
}) as unknown as DataViewListItem[];
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setSavedDataViews(dataViewList);
stateContainer.internalState.dispatch(internalStateActions.setSavedDataViews(dataViewList));
const props = {
stateContainer,
};
@ -41,11 +42,13 @@ describe('DiscoverMainApp', () => {
});
await act(async () => {
const component = await mountWithIntl(
const component = mountWithIntl(
<Router history={history}>
<KibanaContextProvider services={discoverServiceMock}>
<DiscoverMainProvider value={stateContainer}>
<DiscoverMainApp {...props} />
<RuntimeStateProvider currentDataView={dataViewMock} adHocDataViews={[]}>
<DiscoverMainApp {...props} />
</RuntimeStateProvider>
</DiscoverMainProvider>
</KibanaContextProvider>
</Router>
@ -53,7 +56,7 @@ describe('DiscoverMainApp', () => {
// wait for lazy modules
await new Promise((resolve) => setTimeout(resolve, 0));
await component.update();
component.update();
expect(component.find(DiscoverTopNav).exists()).toBe(true);
});

View file

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

View file

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

View file

@ -11,11 +11,11 @@ import { useEffect } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import { DiscoverServices } from '../../../build_services';
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 { DiscoverStateContainer } from '../state_management/discover_state';
import { useFiltersValidation } from './use_filters_validation';
import { useIsEsqlMode } from './use_is_esql_mode';
import { useCurrentDataView } from '../state_management/redux';
export const useAdHocDataViews = ({
services,
@ -23,7 +23,7 @@ export const useAdHocDataViews = ({
stateContainer: DiscoverStateContainer;
services: DiscoverServices;
}) => {
const dataView = useInternalStateSelector((state) => state.dataView);
const dataView = useCurrentDataView();
const savedSearch = useSavedSearch();
const isEsqlMode = useIsEsqlMode();
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 { buildDataTableRecord, EsHitRecord } from '@kbn/discover-utils';
import { omit } from 'lodash';
import { internalStateActions } from '../state_management/redux';
function getHookProps(
query: AggregateQuery | Query | undefined,
@ -37,7 +38,9 @@ function getHookProps(
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.appState.replaceUrlState = replaceUrlState;
stateContainer.appState.update({ columns: [], ...appState });
stateContainer.internalState.transitions.setSavedDataViews([dataViewMock as DataViewListItem]);
stateContainer.internalState.dispatch(
internalStateActions.setSavedDataViews([dataViewMock as DataViewListItem])
);
const msgLoading = {
fetchStatus: defaultFetchStatus,
@ -502,7 +505,9 @@ describe('useEsqlMode', () => {
FetchStatus.LOADING
);
const documents$ = stateContainer.dataState.data$.documents$;
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -517,7 +522,9 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -527,18 +534,22 @@ describe('useEsqlMode', () => {
fetchStatus: FetchStatus.PARTIAL,
query: { esql: 'from pattern1' },
});
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
});
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
})
);
stateContainer.appState.update({ query: { esql: 'from pattern1' } });
documents$.next({
fetchStatus: FetchStatus.LOADING,
query: { esql: 'from pattern1' },
});
await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -554,7 +565,9 @@ describe('useEsqlMode', () => {
query: { esql: 'from pattern2' },
});
await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -571,7 +584,9 @@ describe('useEsqlMode', () => {
const documents$ = stateContainer.dataState.data$.documents$;
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -582,7 +597,9 @@ describe('useEsqlMode', () => {
result: result1,
});
await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
@ -594,7 +611,9 @@ describe('useEsqlMode', () => {
result: result2,
});
await waitFor(() =>
expect(omit(stateContainer.internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(
omit(stateContainer.internalState.getState().resetDefaultProfileState, 'resetId')
).toEqual({
columns: true,
rowHeight: 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 { getValidViewMode } from '../utils/get_valid_view_mode';
import { FetchStatus } from '../../types';
import { internalStateActions, useInternalStateDispatch } from '../state_management/redux';
const MAX_NUM_OF_COLUMNS = 50;
@ -31,6 +32,7 @@ export function useEsqlMode({
stateContainer: DiscoverStateContainer;
dataViews: DataViewsContract;
}) {
const dispatch = useInternalStateDispatch();
const savedSearch = useSavedSearchInitial();
const prev = useRef<{
initialFetch: boolean;
@ -93,11 +95,13 @@ export function useEsqlMode({
// Reset all default profile state when index pattern changes
if (indexPatternChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
});
dispatch(
internalStateActions.setResetDefaultProfileState({
columns: 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
// due to transformational commands, reset the associated default profile state
if (!indexPatternChanged && allColumnsChanged) {
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: false,
breakdownField: false,
});
dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: false,
breakdownField: false,
})
);
}
prev.current.allColumns = nextAllColumns;
@ -186,5 +192,5 @@ export function useEsqlMode({
cleanup();
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 { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
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', () => {
test('inspector open function is executed, expanded doc is closed', async () => {
@ -26,13 +29,22 @@ describe('test useInspector', () => {
const requests = new RequestAdapter();
const lensRequests = new RequestAdapter();
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.internalState.transitions.setExpandedDoc({} as unknown as DataTableRecord);
const { result } = renderHook(() => {
return useInspector({
stateContainer,
inspector: discoverServiceMock.inspector,
});
});
stateContainer.internalState.dispatch(
internalStateActions.setExpandedDoc({} as unknown as DataTableRecord)
);
const { result } = renderHook(
() => {
return useInspector({
stateContainer,
inspector: discoverServiceMock.inspector,
});
},
{
wrapper: ({ children }) => (
<DiscoverMainProvider value={stateContainer}>{children}</DiscoverMainProvider>
),
}
);
await act(async () => {
result.current();
});

View file

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

View file

@ -17,6 +17,7 @@ import { DiscoverServices } from '../../../build_services';
import { DiscoverStateContainer } from '../state_management/discover_state';
import { omit } from 'lodash';
import { createSavedSearchAdHocMock, createSavedSearchMock } from '../../../__mocks__/saved_search';
import { internalStateActions } from '../state_management/redux';
const renderUrlTracking = ({
services,
@ -55,9 +56,11 @@ describe('useUrlTracking', () => {
const services = createDiscoverServicesMock();
const savedSearch = omit(createSavedSearchAdHocMock(), 'id');
const stateContainer = getDiscoverStateMock({ savedSearch });
stateContainer.internalState.transitions.setDefaultProfileAdHocDataViews([
savedSearch.searchSource.getField('index')!,
]);
stateContainer.internalState.dispatch(
internalStateActions.setDefaultProfileAdHocDataViews([
savedSearch.searchSource.getField('index')!,
])
);
expect(services.urlTracker.setTrackingEnabled).not.toHaveBeenCalled();
renderUrlTracking({ services, stateContainer });
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
dataView.isPersisted() ||
// 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
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 { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common';
import { createDataViewDataSource } from '../../../../common/data_sources';
import { getInternalStateContainer } from './discover_internal_state_container';
import {
DiscoverSavedSearchContainer,
getSavedSearchContainer,
} from './discover_saved_search_container';
import { getDiscoverGlobalStateContainer } from './discover_global_state_container';
import { omit } from 'lodash';
import { createInternalStateStore, createRuntimeStateManager, InternalStateStore } from './redux';
let history: History;
let stateStorage: IKbnUrlStateStorage;
let internalState: ReturnType<typeof getInternalStateContainer>;
let internalState: InternalStateStore;
let savedSearchState: DiscoverSavedSearchContainer;
describe('Test discover app state container', () => {
@ -42,18 +42,21 @@ describe('Test discover app state container', () => {
history,
...(toasts && withNotifyOnErrors(toasts)),
});
internalState = getInternalStateContainer();
internalState = createInternalStateStore({
services: discoverServiceMock,
runtimeStateManager: createRuntimeStateManager(),
});
savedSearchState = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer: getDiscoverGlobalStateContainer(stateStorage),
internalStateContainer: internalState,
internalState,
});
});
const getStateContainer = () =>
getDiscoverAppStateContainer({
stateStorage,
internalStateContainer: internalState,
internalState,
savedSearchContainer: savedSearchState,
services: discoverServiceMock,
});
@ -271,13 +274,13 @@ describe('Test discover app state container', () => {
describe('initAndSync', () => {
it('should call setResetDefaultProfileState correctly with no initial state', () => {
const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: true,
breakdownField: true,
@ -288,13 +291,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ columns: ['test'] });
const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: true,
breakdownField: true,
@ -305,13 +308,13 @@ describe('Test discover app state container', () => {
const stateStorageGetSpy = jest.spyOn(stateStorage, 'get');
stateStorageGetSpy.mockReturnValue({ rowHeight: 5 });
const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: true,
rowHeight: false,
breakdownField: true,
@ -328,13 +331,13 @@ describe('Test discover app state container', () => {
managed: false,
});
const state = getStateContainer();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,
});
state.initAndSync();
expect(omit(internalState.get().resetDefaultProfileState, 'resetId')).toEqual({
expect(omit(internalState.getState().resetDefaultProfileState, 'resetId')).toEqual({
columns: false,
rowHeight: false,
breakdownField: false,

View file

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

View file

@ -17,6 +17,7 @@ import { DataDocuments$ } from './discover_data_state_container';
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
import { fetchDocuments } from '../data_fetching/fetch_documents';
import { omit } from 'lodash';
import { internalStateActions } from './redux';
jest.mock('../data_fetching/fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue({ records: [] }),
@ -176,11 +177,13 @@ describe('test getDataStateContainer', () => {
const appUnsub = stateContainer.appState.initAndSync();
await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
});
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
})
);
dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
@ -191,7 +194,9 @@ describe('test getDataStateContainer', () => {
await waitFor(() => {
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,
rowHeight: false,
breakdownField: false,
@ -209,11 +214,13 @@ describe('test getDataStateContainer', () => {
const appUnsub = stateContainer.appState.initAndSync();
await discoverServiceMock.profilesManager.resolveDataSourceProfile({});
stateContainer.actions.setDataView(dataViewMock);
stateContainer.internalState.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
});
stateContainer.internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
})
);
dataState.data$.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: 0,
@ -222,7 +229,9 @@ describe('test getDataStateContainer', () => {
await waitFor(() => {
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,
rowHeight: 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 { sendResetMsg } from '../hooks/use_saved_search_messages';
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 { internalStateActions, InternalStateStore, RuntimeStateManager } from './redux';
export interface SavedSearchData {
main$: DataMain$;
@ -138,14 +138,16 @@ export function getDataStateContainer({
services,
searchSessionManager,
appStateContainer,
internalStateContainer,
internalState,
runtimeStateManager,
getSavedSearch,
setDataView,
}: {
services: DiscoverServices;
searchSessionManager: DiscoverSearchSessionManager;
appStateContainer: DiscoverAppStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
getSavedSearch: () => SavedSearch;
setDataView: (dataView: DataView) => void;
}): DiscoverDataStateContainer {
@ -229,7 +231,7 @@ export function getDataStateContainer({
searchSessionId,
services,
getAppState: appStateContainer.getState,
getInternalState: internalStateContainer.getState,
internalState,
savedSearch: getSavedSearch(),
};
@ -254,10 +256,12 @@ export function getDataStateContainer({
return;
}
internalStateContainer.transitions.setDataRequestParams({
timeRangeAbsolute: timefilter.getAbsoluteTime(),
timeRangeRelative: timefilter.getTime(),
});
internalState.dispatch(
internalStateActions.setDataRequestParams({
timeRangeAbsolute: timefilter.getAbsoluteTime(),
timeRangeRelative: timefilter.getTime(),
})
);
await profilesManager.resolveDataSourceProfile({
dataSource: appStateContainer.getState().dataSource,
@ -265,7 +269,8 @@ export function getDataStateContainer({
query: appStateContainer.getState().query,
});
const { resetDefaultProfileState, dataView } = internalStateContainer.getState();
const { resetDefaultProfileState } = internalState.getState();
const dataView = runtimeStateManager.currentDataView$.getValue();
const defaultProfileState = dataView
? getDefaultProfileState({ profilesManager, resetDefaultProfileState, dataView })
: undefined;
@ -294,7 +299,7 @@ export function getDataStateContainer({
},
async () => {
const { resetDefaultProfileState: currentResetDefaultProfileState } =
internalStateContainer.getState();
internalState.getState();
if (currentResetDefaultProfileState.resetId !== resetDefaultProfileState.resetId) {
return;
@ -313,11 +318,13 @@ export function getDataStateContainer({
// Clear the default profile state flags after the data fetching
// is done so refetches don't reset the state again
internalStateContainer.transitions.setResetDefaultProfileState({
columns: false,
rowHeight: false,
breakdownField: false,
});
internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: 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 { VIEW_MODE } from '../../../../common/constants';
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', () => {
const savedSearch = savedSearchMock;
const services = discoverServiceMock;
const globalStateContainer = getDiscoverGlobalStateContainer(createKbnUrlStateStorage());
const internalStateContainer = getInternalStateContainer();
const internalState = createInternalStateStore({
services,
runtimeStateManager: createRuntimeStateManager(),
});
describe('getTitle', () => {
it('returns undefined for new saved searches', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
expect(container.getTitle()).toBe(undefined);
});
@ -39,7 +43,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
container.set(savedSearch);
expect(container.getTitle()).toBe(savedSearch.title);
@ -51,7 +56,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
const result = container.set(newSavedSearch);
@ -68,7 +74,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
const newSavedSearch: SavedSearch = { ...savedSearch, title: 'New title' };
@ -82,7 +89,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
const result = await container.new(dataViewMock);
@ -99,7 +107,8 @@ describe('DiscoverSavedSearchContainer', () => {
const container = getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
});
const result = await container.new(dataViewMock);
expect(result.title).toBeUndefined();
@ -119,7 +128,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
await savedSearchContainer.load('the-saved-search-id');
expect(savedSearchContainer.getInitial$().getValue().id).toEqual('the-saved-search-id');
@ -135,7 +145,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
const savedSearchToPersist = {
...savedSearchMockWithTimeField,
@ -161,7 +172,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
const result = await savedSearchContainer.persist(persistedSavedSearch, saveOptions);
@ -174,7 +186,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
const savedSearchToPersist = {
...savedSearchMockWithTimeField,
@ -194,7 +207,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
const savedSearchToPersist = {
...savedSearchMockWithTimeField,
@ -219,7 +233,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
savedSearchContainer.set(savedSearch);
savedSearchContainer.update({ nextState: { hideChart: true } });
@ -241,7 +256,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
savedSearchContainer.set(savedSearch);
const updated = savedSearchContainer.update({ nextState: { hideChart: true } });
@ -257,7 +273,8 @@ describe('DiscoverSavedSearchContainer', () => {
const savedSearchContainer = getSavedSearchContainer({
services: discoverServiceMock,
globalStateContainer,
internalStateContainer,
internalState,
});
const updated = savedSearchContainer.update({ nextDataView: dataViewMock });
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 { getStateDefaults } from './utils/get_state_defaults';
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 = {
...COMPARE_ALL_OPTIONS,
@ -139,11 +139,11 @@ export interface DiscoverSavedSearchContainer {
export function getSavedSearchContainer({
services,
globalStateContainer,
internalStateContainer,
internalState,
}: {
services: DiscoverServices;
globalStateContainer: DiscoverGlobalStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
internalState: InternalStateStore;
}): DiscoverSavedSearchContainer {
const initialSavedSearch = services.savedSearch.getNew();
const savedSearchInitial$ = new BehaviorSubject(initialSavedSearch);
@ -183,7 +183,7 @@ export function getSavedSearchContainer({
addLog('[savedSearch] persist', { nextSavedSearch, saveOptions });
const dataView = nextSavedSearch.searchSource.getField('index');
const profileDataViewIds = internalStateContainer.getState().defaultProfileAdHocDataViewIds;
const profileDataViewIds = internalState.getState().defaultProfileAdHocDataViewIds;
let replacementDataView: DataView | undefined;
// 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 { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context';
import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources';
import { createRuntimeStateManager } from './redux';
const startSync = (appState: DiscoverAppStateContainer) => {
const { start, stop } = appState.syncState();
@ -46,17 +47,22 @@ async function getState(
const nextHistory = createBrowserHistory();
nextHistory.push(url);
discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({
...dataViewMock,
isPersisted: () => false,
id: 'ad-hoc-id',
title: 'test',
discoverServiceMock.dataViews.create = jest.fn().mockImplementation((spec) => {
spec.id = spec.id ?? 'ad-hoc-id';
spec.title = spec.title ?? 'test';
return Promise.resolve({
...dataViewMock,
isPersisted: () => false,
toSpec: () => spec,
...spec,
});
});
const runtimeStateManager = createRuntimeStateManager();
const nextState = getDiscoverStateContainer({
services: discoverServiceMock,
history: nextHistory,
customizationContext: mockCustomizationContext,
runtimeStateManager,
});
nextState.appState.isEmptyURL = jest.fn(() => isEmptyUrl ?? true);
jest.spyOn(nextState.dataState, 'fetch');
@ -77,6 +83,7 @@ async function getState(
return {
history: nextHistory,
state: nextState,
runtimeStateManager,
getCurrentUrl,
};
}
@ -94,6 +101,7 @@ describe('Test discover state', () => {
services: discoverServiceMock,
history,
customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
});
state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true);
@ -192,6 +200,7 @@ describe('Test discover state with overridden state storage', () => {
history,
customizationContext: mockCustomizationContext,
stateStorageContainer: stateStorage,
runtimeStateManager: createRuntimeStateManager(),
});
state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true);
@ -283,6 +292,7 @@ describe('Test createSearchSessionRestorationDataProvider', () => {
services: discoverServiceMock,
history,
customizationContext: mockCustomizationContext,
runtimeStateManager: createRuntimeStateManager(),
});
discoverStateContainer.appState.update({
dataSource: createDataViewDataSource({
@ -419,9 +429,11 @@ describe('Test discover state actions', () => {
});
test('setDataView', async () => {
const { state } = await getState('');
const { state, runtimeStateManager } = await getState('');
expect(runtimeStateManager.currentDataView$.getValue()).toBeUndefined();
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 () => {
@ -717,7 +729,7 @@ describe('Test discover state actions', () => {
state.savedSearchState.getCurrent$().getValue().searchSource?.getField('index')?.id
).toEqual(dataViewSpecMock.id);
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 () => {
@ -749,7 +761,7 @@ describe('Test discover state actions', () => {
expect(state.appState.getState().dataSource).toEqual(
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 () => {
@ -829,7 +841,7 @@ describe('Test discover state actions', () => {
const unsubscribe = state.actions.initializeAndSync();
await state.actions.onDataViewCreated(dataViewComplexMock);
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(
createDataViewDataSource({ dataViewId: dataViewComplexMock.id! })
@ -844,9 +856,14 @@ describe('Test discover state actions', () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
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 waitFor(() => {
expect(state.internalState.getState().dataView?.id).toBe(dataViewAdHoc.id);
expect(state.internalState.getState().dataViewId).toBe(dataViewAdHoc.id);
});
expect(state.appState.getState().dataSource).toEqual(
createDataViewDataSource({ dataViewId: dataViewAdHoc.id! })
@ -860,15 +877,12 @@ describe('Test discover state actions', () => {
test('onDataViewEdited - persisted data view', async () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
const selectedDataView = state.internalState.getState().dataView;
await waitFor(() => {
expect(selectedDataView).toBe(dataViewMock);
});
const selectedDataViewId = state.internalState.getState().dataViewId;
expect(selectedDataViewId).toBe(dataViewMock.id);
const unsubscribe = state.actions.initializeAndSync();
await state.actions.onDataViewEdited(dataViewMock);
await waitFor(() => {
expect(state.internalState.getState().dataView).not.toBe(selectedDataView);
expect(state.internalState.getState().dataViewId).toBe(selectedDataViewId);
});
unsubscribe();
});
@ -880,7 +894,7 @@ describe('Test discover state actions', () => {
const previousId = dataViewAdHoc.id;
await state.actions.onDataViewEdited(dataViewAdHoc);
await waitFor(() => {
expect(state.internalState.getState().dataView?.id).not.toBe(previousId);
expect(state.internalState.getState().dataViewId).not.toBe(previousId);
});
unsubscribe();
});
@ -916,7 +930,7 @@ describe('Test discover state actions', () => {
expect(state.appState.getState().dataSource).toEqual(
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();
});
@ -929,7 +943,7 @@ describe('Test discover state actions', () => {
const initialUrlState =
'/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())';
expect(getCurrentUrl()).toBe(initialUrlState);
expect(state.internalState.getState().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
await state.actions.onChangeDataView(dataViewComplexMock.id!);
@ -940,7 +954,7 @@ describe('Test discover state actions', () => {
await waitFor(() => {
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
await state.actions.undoSavedSearchChanges();
@ -949,7 +963,7 @@ describe('Test discover state actions', () => {
await waitFor(() => {
expect(state.dataState.fetch).toHaveBeenCalledTimes(2);
});
expect(state.internalState.getState().dataView?.id).toBe(dataViewMock.id!);
expect(state.internalState.getState().dataViewId).toBe(dataViewMock.id!);
unsubscribe();
});
@ -991,6 +1005,7 @@ describe('Test discover state with embedded mode', () => {
...mockCustomizationContext,
displayMode: 'embedded',
},
runtimeStateManager: createRuntimeStateManager(),
});
state.savedSearchState.set(savedSearchMock);
state.appState.update({}, true);

View file

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

View file

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

View file

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

View file

@ -16,72 +16,79 @@ import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../../__mocks__/services';
import type { DataView } from '@kbn/data-views-plugin/common';
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 { createRuntimeStateManager, internalStateActions } from '../redux';
const setupTestParams = (dataView: DataView | undefined) => {
const savedSearch = savedSearchMock;
const services = discoverServiceMock;
const discoverState = getDiscoverStateMock({
savedSearch,
});
discoverState.internalState.transitions.setDataView(savedSearch.searchSource.getField('index')!);
const runtimeStateManager = createRuntimeStateManager();
const discoverState = getDiscoverStateMock({ savedSearch, runtimeStateManager });
discoverState.internalState.dispatch(
internalStateActions.setDataView(savedSearch.searchSource.getField('index')!)
);
services.dataViews.get = jest.fn(() => Promise.resolve(dataView as DataView));
discoverState.appState.update = jest.fn();
discoverState.internalState.transitions = {
setIsDataViewLoading: jest.fn(),
setResetDefaultProfileState: jest.fn(),
} as unknown as Readonly<PureTransitionsToTransitions<InternalStateTransitions>>;
return {
services,
appState: discoverState.appState,
internalState: discoverState.internalState,
runtimeStateManager,
};
};
describe('changeDataView', () => {
it('should set the right app state when a valid data view (which includes the preconfigured default column) to switch to is given', async () => {
const params = setupTestParams(dataViewWithDefaultColumnMock);
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({
columns: ['default_column'], // default_column would be added as dataViewWithDefaultColumn has it as a mapped field
dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-user-default-column-id' }),
sort: [['@timestamp', 'desc']],
});
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
expect(params.internalState.getState().isDataViewLoading).toBe(false);
});
it('should set the right app state when a valid data view to switch to is given', async () => {
const params = setupTestParams(dataViewComplexMock);
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({
columns: [], // default_column would not be added as dataViewComplexMock does not have it as a mapped field
dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-various-field-types-id' }),
sort: [['data', 'desc']],
});
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
expect(params.internalState.getState().isDataViewLoading).toBe(false);
});
it('should not set the app state when an invalid data view to switch to is given', async () => {
const params = setupTestParams(undefined);
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.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true);
expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false);
expect(params.internalState.getState().isDataViewLoading).toBe(false);
});
it('should call setResetDefaultProfileState correctly when switching data view', async () => {
const params = setupTestParams(dataViewComplexMock);
expect(params.internalState.transitions.setResetDefaultProfileState).not.toHaveBeenCalled();
await changeDataView(dataViewComplexMock.id!, params);
expect(params.internalState.transitions.setResetDefaultProfileState).toHaveBeenCalledWith({
columns: true,
rowHeight: true,
breakdownField: true,
});
expect(params.internalState.getState().resetDefaultProfileState).toEqual(
expect.objectContaining({
columns: false,
rowHeight: false,
breakdownField: false,
})
);
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,
DEFAULT_COLUMNS_SETTING,
} from '@kbn/discover-utils';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverAppStateContainer } from '../discover_app_state_container';
import { addLog } from '../../../../utils/add_log';
import { DiscoverServices } from '../../../../build_services';
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
*/
export async function changeDataView(
id: string | DataView,
{
services,
internalState,
appState,
}: {
services: DiscoverServices;
internalState: DiscoverInternalStateContainer;
appState: DiscoverAppStateContainer;
}
) {
addLog('[ui] changeDataView', { id });
export async function changeDataView({
dataViewId,
services,
internalState,
runtimeStateManager,
appState,
}: {
dataViewId: string | DataView;
services: DiscoverServices;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
appState: DiscoverAppStateContainer;
}) {
addLog('[ui] changeDataView', { id: dataViewId });
const { dataViews, uiSettings } = services;
const dataView = internalState.getState().dataView;
const currentDataView = runtimeStateManager.currentDataView$.getValue();
const state = appState.getState();
let nextDataView: DataView | null = null;
internalState.transitions.setIsDataViewLoading(true);
internalState.dispatch(internalStateActions.setIsDataViewLoading(true));
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.
// 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
}
if (nextDataView && dataView) {
if (nextDataView && currentDataView) {
// Reset the default profile state if we are switching to a different data view
internalState.transitions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
});
internalState.dispatch(
internalStateActions.setResetDefaultProfileState({
columns: true,
rowHeight: true,
breakdownField: true,
})
);
const nextAppState = getDataViewAppState(
dataView,
currentDataView,
nextDataView,
uiSettings.get(DEFAULT_COLUMNS_SETTING, []),
state.columns || [],
@ -79,9 +83,9 @@ export async function changeDataView(
appState.update(nextAppState);
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,
ProfilesManager,
} from '../../../../context_awareness';
import type { InternalState } from '../discover_internal_state_container';
import type { DataDocumentsMsg } from '../discover_data_state_container';
import type { DiscoverInternalState } from '../redux';
export const getDefaultProfileState = ({
profilesManager,
@ -25,7 +25,7 @@ export const getDefaultProfileState = ({
dataView,
}: {
profilesManager: ProfilesManager;
resetDefaultProfileState: InternalState['resetDefaultProfileState'];
resetDefaultProfileState: DiscoverInternalState['resetDefaultProfileState'];
dataView: DataView;
}) => {
const defaultState = getDefaultState(profilesManager, dataView);

View file

@ -12,7 +12,6 @@ import { cloneDeep, isEqual } from 'lodash';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getEsqlDataView } from './get_esql_data_view';
import { loadAndResolveDataView } from './resolve_data_view';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverDataStateContainer } from '../discover_data_state_container';
import { cleanupUrlState } from './cleanup_url_state';
import { getValidFilters } from '../../../../utils/get_valid_filters';
@ -27,11 +26,13 @@ import {
import { DiscoverGlobalStateContainer } from '../discover_global_state_container';
import { DiscoverServices } from '../../../../build_services';
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
import { InternalStateStore, RuntimeStateManager, internalStateActions } from '../redux';
interface LoadSavedSearchDeps {
appStateContainer: DiscoverAppStateContainer;
dataStateContainer: DiscoverDataStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
savedSearchContainer: DiscoverSavedSearchContainer;
globalStateContainer: DiscoverGlobalStateContainer;
services: DiscoverServices;
@ -51,7 +52,8 @@ export const loadSavedSearch = async (
const { savedSearchId, initialAppState } = params ?? {};
const {
appStateContainer,
internalStateContainer,
internalState,
runtimeStateManager,
savedSearchContainer,
globalStateContainer,
services,
@ -75,7 +77,8 @@ export const loadSavedSearch = async (
dataViewId,
query: appState?.query,
services,
internalStateContainer,
internalState,
runtimeStateManager,
})
);
}
@ -110,7 +113,8 @@ export const loadSavedSearch = async (
query: appState.query,
savedSearch: nextSavedSearch,
services,
internalStateContainer,
internalState,
runtimeStateManager,
});
const dataViewDifferentToAppState = stateDataView.id !== savedSearchDataViewId;
if (
@ -142,7 +146,7 @@ export const loadSavedSearch = async (
nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters();
}
internalStateContainer.transitions.resetOnSavedSearchChange();
internalState.dispatch(internalStateActions.resetOnSavedSearchChange());
return nextSavedSearch;
};
@ -153,12 +157,12 @@ export const loadSavedSearch = async (
* @param deps
*/
function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps) {
const { dataStateContainer, internalStateContainer, services, setDataView } = deps;
const { dataStateContainer, internalState, services, setDataView } = deps;
const savedSearchDataView = savedSearch.searchSource.getField('index')!;
setDataView(savedSearchDataView);
if (!savedSearchDataView.isPersisted()) {
internalStateContainer.transitions.appendAdHocDataViews(savedSearchDataView);
internalState.dispatch(internalStateActions.appendAdHocDataViews(savedSearchDataView));
}
// Finally notify dataStateContainer, data.query and filterManager about new derived state
@ -196,13 +200,15 @@ const getStateDataView = async (
query,
savedSearch,
services,
internalStateContainer,
internalState,
runtimeStateManager,
}: {
dataViewId?: string;
query: DiscoverAppState['query'];
savedSearch?: SavedSearch;
services: DiscoverServices;
internalStateContainer: DiscoverInternalStateContainer;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
}
) => {
const { dataView, dataViewSpec } = params;
@ -222,7 +228,8 @@ const getStateDataView = async (
savedSearch,
isEsqlMode: isEsqlQuery,
services,
internalStateContainer,
internalState,
runtimeStateManager,
});
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 { ToastsStart } from '@kbn/core/public';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { DiscoverInternalStateContainer } from '../discover_internal_state_container';
import { DiscoverServices } from '../../../../build_services';
import { InternalStateStore, RuntimeStateManager } from '../redux';
interface DataViewData {
/**
@ -174,18 +174,21 @@ export const loadAndResolveDataView = async ({
dataViewSpec,
savedSearch,
isEsqlMode,
internalStateContainer,
internalState,
runtimeStateManager,
services,
}: {
dataViewId?: string;
dataViewSpec?: DataViewSpec;
savedSearch?: SavedSearch;
isEsqlMode?: boolean;
internalStateContainer: DiscoverInternalStateContainer;
internalState: InternalStateStore;
runtimeStateManager: RuntimeStateManager;
services: DiscoverServices;
}) => {
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,
// 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 { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
import { discoverServiceMock } from '../../__mocks__/services';
import { DataView } from '@kbn/data-views-plugin/common';
import React from 'react';
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 = ({
rootProfileLoading,
@ -23,20 +25,22 @@ const renderDefaultAdHocDataViewsHook = ({
const clearInstanceCache = jest.spyOn(discoverServiceMock.dataViews, 'clearInstanceCache');
const createDataView = jest
.spyOn(discoverServiceMock.dataViews, 'create')
.mockImplementation((spec) => Promise.resolve(spec as unknown as DataView));
const existingAdHocDataVew = { id: '1', title: 'test' } as unknown as DataView;
const previousSpecs = [
{ id: '2', title: 'tes2' },
{ id: '3', title: 'test3' },
.mockImplementation((spec) => Promise.resolve(buildDataViewMock(omit(spec, 'fields'))));
const existingAdHocDataVew = buildDataViewMock({ id: '1', title: 'test' });
const previousDataViews = [
buildDataViewMock({ id: '2', title: 'tes2' }),
buildDataViewMock({ id: '3', title: 'test3' }),
];
const newSpecs = [
{ id: '4', title: 'test4' },
{ id: '5', title: 'test5' },
const newDataViews = [
buildDataViewMock({ id: '4', title: 'test4' }),
buildDataViewMock({ id: '5', title: 'test5' }),
];
const stateContainer = getDiscoverStateMock({});
stateContainer.internalState.transitions.appendAdHocDataViews(existingAdHocDataVew);
stateContainer.internalState.transitions.setDefaultProfileAdHocDataViews(
previousSpecs as unknown as DataView[]
stateContainer.internalState.dispatch(
internalStateActions.appendAdHocDataViews(existingAdHocDataVew)
);
stateContainer.internalState.dispatch(
internalStateActions.setDefaultProfileAdHocDataViews(previousDataViews)
);
const { result, unmount } = renderHook(useDefaultAdHocDataViews, {
initialProps: {
@ -44,7 +48,11 @@ const renderDefaultAdHocDataViewsHook = ({
rootProfileState: {
rootProfileLoading,
AppWrapper: () => null,
getDefaultAdHocDataViews: () => newSpecs,
getDefaultAdHocDataViews: () =>
newDataViews.map((dv) => {
const { id, ...restSpec } = dv.toSpec();
return { id: id!, ...restSpec };
}),
},
},
wrapper: ({ children }) => (
@ -58,8 +66,8 @@ const renderDefaultAdHocDataViewsHook = ({
createDataView,
stateContainer,
existingAdHocDataVew,
previousSpecs,
newSpecs,
previousDataViews,
newDataViews,
};
};
@ -75,27 +83,26 @@ describe('useDefaultAdHocDataViews', () => {
createDataView,
stateContainer,
existingAdHocDataVew,
previousSpecs,
newSpecs,
previousDataViews,
newDataViews,
} = renderDefaultAdHocDataViewsHook({ rootProfileLoading: false });
expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([
expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew,
...previousSpecs,
...previousDataViews,
]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id)
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousDataViews.map((dv) => dv.id)
);
await result.current.initializeProfileDataViews();
expect(clearInstanceCache.mock.calls).toEqual(previousSpecs.map((s) => [s.id]));
expect(createDataView.mock.calls).toEqual(newSpecs.map((s) => [s, true]));
expect(stateContainer.internalState.get().adHocDataViews).toEqual([
existingAdHocDataVew,
...newSpecs,
]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual(
newSpecs.map((s) => s.id)
expect(clearInstanceCache.mock.calls).toEqual(previousDataViews.map((dv) => [dv.id]));
expect(createDataView.mock.calls).toEqual(newDataViews.map((dv) => [dv.toSpec(), true]));
expect(
stateContainer.runtimeStateManager.adHocDataViews$.getValue().map((dv) => dv.id)
).toEqual([existingAdHocDataVew.id, ...newDataViews.map((dv) => dv.id)]);
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
newDataViews.map((dv) => dv.id)
);
});
@ -106,43 +113,45 @@ describe('useDefaultAdHocDataViews', () => {
createDataView,
stateContainer,
existingAdHocDataVew,
previousSpecs,
previousDataViews,
} = renderDefaultAdHocDataViewsHook({ rootProfileLoading: true });
expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([
expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew,
...previousSpecs,
...previousDataViews,
]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id)
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousDataViews.map((dv) => dv.id)
);
await result.current.initializeProfileDataViews();
expect(clearInstanceCache).not.toHaveBeenCalled();
expect(createDataView).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([
expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew,
...previousSpecs,
...previousDataViews,
]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id)
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousDataViews.map((dv) => dv.id)
);
});
it('should clear instance cache on unmount', async () => {
const { unmount, clearInstanceCache, stateContainer, existingAdHocDataVew, previousSpecs } =
const { unmount, clearInstanceCache, stateContainer, existingAdHocDataVew, previousDataViews } =
renderDefaultAdHocDataViewsHook({ rootProfileLoading: false });
expect(clearInstanceCache).not.toHaveBeenCalled();
expect(stateContainer.internalState.get().adHocDataViews).toEqual([
expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).toEqual([
existingAdHocDataVew,
...previousSpecs,
...previousDataViews,
]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual(
previousSpecs.map((s) => s.id)
expect(stateContainer.internalState.getState().defaultProfileAdHocDataViewIds).toEqual(
previousDataViews.map((dv) => dv.id)
);
unmount();
expect(clearInstanceCache.mock.calls).toEqual(previousSpecs.map((s) => [s.id]));
expect(stateContainer.internalState.get().adHocDataViews).toEqual([existingAdHocDataVew]);
expect(stateContainer.internalState.get().defaultProfileAdHocDataViewIds).toEqual([]);
expect(clearInstanceCache.mock.calls).toEqual(previousDataViews.map((s) => [s.id]));
expect(stateContainer.runtimeStateManager.adHocDataViews$.getValue()).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 { useDiscoverServices } from '../../hooks/use_discover_services';
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
@ -36,7 +37,7 @@ export const useDefaultAdHocDataViews = ({
// Clear the cache of old data views before creating
// 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);
}
@ -45,7 +46,7 @@ export const useDefaultAdHocDataViews = ({
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
@ -53,11 +54,11 @@ export const useDefaultAdHocDataViews = ({
// Make sure to clean up on unmount
useUnmount(() => {
for (const prevId of internalState.get().defaultProfileAdHocDataViewIds) {
for (const prevId of internalState.getState().defaultProfileAdHocDataViewIds) {
dataViews.clearInstanceCache(prevId);
}
internalState.transitions.setDefaultProfileAdHocDataViews([]);
internalState.dispatch(internalStateActions.setDefaultProfileAdHocDataViews([]));
});
return { initializeProfileDataViews };

View file

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

View file

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

View file

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

View file

@ -20,7 +20,6 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const dataGrid = getService('dataGrid');
const retry = getService('retry');
const defaultSettings = { defaultIndex: 'logstash-*' };
@ -69,33 +68,5 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await browser.goBack();
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 { 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';
export interface SecuritySolutionDiscoverState {
app: DiscoverAppState | undefined;
internal: InternalState | undefined;
internal: DiscoverInternalState | 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 { createGlobalStyle } from 'styled-components';
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 { useQuery } from '@tanstack/react-query';
import { isEqualWith } from 'lodash';
@ -219,7 +219,7 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
next: setDiscoverAppState,
});
const internalStateSubscription = stateContainer.internalState.state$.subscribe({
const internalStateSubscription = from(stateContainer.internalState).subscribe({
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 { 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 { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -22,7 +22,7 @@ export const useDiscoverState = () => {
const result = state.discover.app;
return result;
});
const discoverInternalState = useSelector<State, InternalState | undefined>((state) => {
const discoverInternalState = useSelector<State, DiscoverInternalState | undefined>((state) => {
const result = state.discover.internal;
return result;
});
@ -41,7 +41,7 @@ export const useDiscoverState = () => {
);
const setDiscoverInternalState = useCallback(
(newState: InternalState) => {
(newState: DiscoverInternalState) => {
dispatch(updateDiscoverInternalState({ newState }));
},
[dispatch]

View file

@ -17,7 +17,6 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const dataGrid = getService('dataGrid');
const retry = getService('retry');
const defaultSettings = { defaultIndex: 'logstash-*' };
@ -67,33 +66,5 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await browser.goBack();
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);
});
});
};