[Discover] Fix tab preview reactivity (#218368)

## Summary

This PR fixes the issue with Discover tabs where `getPreviewData` is not
reactive. It's not as straightforward as it probably should be because
of our nested state containers, but it should be reliable for now. I
experimented with putting the `previewDataMap$` observable directly in
`RuntimeStateManager`, but it seemed trickier with more manual syncing
involved, so I just put it in a hook for now.

Fixes #217120.

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Davis McPhee 2025-04-16 19:02:03 -03:00 committed by GitHub
parent bcba741abc
commit de49dec324
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 111 additions and 34 deletions

View file

@ -7,22 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { type TabItem, UnifiedTabs, TabStatus } from '@kbn/unified-tabs';
import { type TabItem, UnifiedTabs } from '@kbn/unified-tabs';
import React, { useState } from 'react';
import { pick } from 'lodash';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view';
import {
CurrentTabProvider,
createTabItem,
internalStateActions,
selectAllTabs,
selectTabRuntimeState,
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { FetchStatus } from '../../../types';
import { usePreviewData } from './use_preview_data';
export const TabsView = (props: DiscoverSessionViewProps) => {
const services = useDiscoverServices();
@ -30,6 +28,7 @@ export const TabsView = (props: DiscoverSessionViewProps) => {
const allTabs = useInternalStateSelector(selectAllTabs);
const currentTabId = useInternalStateSelector((state) => state.tabs.unsafeCurrentId);
const [initialItems] = useState<TabItem[]>(() => allTabs.map((tab) => pick(tab, 'id', 'label')));
const { getPreviewData } = usePreviewData(props.runtimeStateManager);
return (
<UnifiedTabs
@ -40,36 +39,7 @@ export const TabsView = (props: DiscoverSessionViewProps) => {
return dispatch(updateTabsAction);
}}
createItem={() => createTabItem(allTabs)}
getPreviewData={(item) => {
const defaultQuery = { language: 'kuery', query: '(Empty query)' };
const stateContainer = selectTabRuntimeState(
props.runtimeStateManager,
item.id
).stateContainer$.getValue();
if (!stateContainer) {
return {
query: defaultQuery,
status: TabStatus.RUNNING,
};
}
const fetchStatus = stateContainer.dataState.data$.main$.getValue().fetchStatus;
const query = stateContainer.appState.getState().query;
return {
query: isOfAggregateQueryType(query)
? { esql: query.esql.trim() || defaultQuery.query }
: query
? { ...query, query: query.query.trim() || defaultQuery.query }
: defaultQuery,
status: [FetchStatus.UNINITIALIZED, FetchStatus.COMPLETE].includes(fetchStatus)
? TabStatus.SUCCESS
: fetchStatus === FetchStatus.ERROR
? TabStatus.ERROR
: TabStatus.RUNNING,
};
}}
getPreviewData={getPreviewData}
renderContent={() => (
<CurrentTabProvider currentTabId={currentTabId}>
<DiscoverSessionView key={currentTabId} {...props} />

View file

@ -0,0 +1,107 @@
/*
* 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 { useCallback, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import { combineLatest, distinctUntilChanged, map, of, startWith, switchMap } from 'rxjs';
import type { TabItem, TabPreviewData } from '@kbn/unified-tabs';
import { TabStatus } from '@kbn/unified-tabs';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { isEqual } from 'lodash';
import type { RuntimeStateManager } from '../../state_management/redux';
import { selectTabRuntimeState, useInternalStateSelector } from '../../state_management/redux';
import { FetchStatus } from '../../../types';
export const usePreviewData = (runtimeStateManager: RuntimeStateManager) => {
const allTabIds = useInternalStateSelector((state) => state.tabs.allIds);
const previewDataMap$ = useMemo(
() =>
combineLatest(
allTabIds.reduce<Record<string, Observable<TabPreviewData>>>(
(acc, tabId) => ({
...acc,
[tabId]: getPreviewDataObservable(runtimeStateManager, tabId),
}),
{}
)
),
[allTabIds, runtimeStateManager]
);
const previewDataMap = useObservable(previewDataMap$);
const getPreviewData = useCallback(
(item: TabItem) =>
previewDataMap?.[item.id] ?? {
status: TabStatus.SUCCESS,
query: DEFAULT_PREVIEW_QUERY,
},
[previewDataMap]
);
return { getPreviewData };
};
const getPreviewStatus = (fetchStatus: FetchStatus): TabPreviewData['status'] => {
switch (fetchStatus) {
case FetchStatus.UNINITIALIZED:
case FetchStatus.COMPLETE:
return TabStatus.SUCCESS;
case FetchStatus.ERROR:
return TabStatus.ERROR;
default:
return TabStatus.RUNNING;
}
};
const DEFAULT_PREVIEW_QUERY = {
language: 'kuery',
query: i18n.translate('discover.tabsView.defaultQuery', { defaultMessage: '(Empty query)' }),
};
const getPreviewQuery = (query: TabPreviewData['query'] | undefined): TabPreviewData['query'] => {
if (!query) {
return DEFAULT_PREVIEW_QUERY;
}
if (isOfAggregateQueryType(query)) {
return {
...query,
esql: query.esql.trim() || DEFAULT_PREVIEW_QUERY.query,
};
}
return {
...query,
query: query.query.trim() || DEFAULT_PREVIEW_QUERY.query,
};
};
const getPreviewDataObservable = (runtimeStateManager: RuntimeStateManager, tabId: string) =>
selectTabRuntimeState(runtimeStateManager, tabId).stateContainer$.pipe(
switchMap((tabStateContainer) => {
if (!tabStateContainer) {
return of({ status: TabStatus.RUNNING, query: DEFAULT_PREVIEW_QUERY });
}
const { appState } = tabStateContainer;
return combineLatest([
tabStateContainer.dataState.data$.main$,
appState.state$.pipe(startWith(appState.get())),
]).pipe(
map(([{ fetchStatus }, { query }]) => ({ fetchStatus, query })),
distinctUntilChanged(isEqual),
map(({ fetchStatus, query }) => ({
status: getPreviewStatus(fetchStatus),
query: getPreviewQuery(query),
}))
);
})
);