[embeddable] cleanup usages of useBatchedOptionalPublishingSubjects (#216714)

`useBatchedOptionalPublishingSubjects` should only be used when `api` is
not available until after rendering. This PR replaces usages of
`useBatchedOptionalPublishingSubjects` with
`useBatchedPublishingSubjects` where possible.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2025-05-07 13:32:33 -06:00 committed by GitHub
parent 670ff4ee06
commit e269d04ee0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 84 additions and 55 deletions

View file

@ -22,51 +22,80 @@ import {
} from '@elastic/eui';
import { BehaviorSubject, Subject } from 'rxjs';
import { TimeRange } from '@kbn/es-query';
import { useBatchedOptionalPublishingSubjects } from '@kbn/presentation-publishing';
import { PublishesDataLoading, useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { SearchEmbeddableRenderer } from '../react_embeddables/search/search_embeddable_renderer';
import { SEARCH_EMBEDDABLE_TYPE } from '../react_embeddables/search/constants';
import type { SearchApi, SearchSerializedState } from '../react_embeddables/search/types';
function DatePicker({
dataLoading$,
onReload,
timeRange,
setTimeRange,
}: {
dataLoading$: PublishesDataLoading['dataLoading$'];
timeRange: TimeRange;
setTimeRange: (timeRange: TimeRange) => void;
onReload: () => void;
}) {
const dataLoading = useStateFromPublishingSubject(dataLoading$);
return (
<EuiSuperDatePicker
isLoading={dataLoading ?? false}
start={timeRange.from}
end={timeRange.to}
onTimeChange={({ start, end }: OnTimeChangeProps) => {
setTimeRange({
from: start,
to: end,
});
}}
onRefresh={() => {
onReload();
}}
/>
);
}
export const RenderExamples = () => {
const parentApi = useMemo(() => {
const timeRange$ = new BehaviorSubject<TimeRange>({
from: 'now-24h',
to: 'now',
});
const reload$ = new Subject<void>();
return {
reload$: new Subject<void>(),
reload$,
onReload: () => {
reload$.next();
},
getSerializedStateForChild: () => ({
rawState: {
timeRange: undefined,
},
}),
timeRange$: new BehaviorSubject<TimeRange>({
from: 'now-24h',
to: 'now',
}),
timeRange$,
setTimeRange: (timeRange: TimeRange | undefined) => {
if (timeRange) timeRange$.next(timeRange);
},
};
// only run onMount
}, []);
const [api, setApi] = useState<SearchApi | null>(null);
const [hidePanelChrome, setHidePanelChrome] = useState<boolean>(false);
const [dataLoading, timeRange] = useBatchedOptionalPublishingSubjects(
api?.dataLoading$,
parentApi.timeRange$
);
const timeRange = useStateFromPublishingSubject(parentApi.timeRange$);
return (
<div>
<EuiSuperDatePicker
isLoading={dataLoading ? dataLoading : false}
start={timeRange.from}
end={timeRange.to}
onTimeChange={({ start, end }: OnTimeChangeProps) => {
parentApi.timeRange$.next({
from: start,
to: end,
});
}}
onRefresh={() => {
parentApi.reload$.next();
}}
/>
{api && (
<DatePicker
dataLoading$={api.dataLoading$}
onReload={parentApi.onReload}
setTimeRange={parentApi.setTimeRange}
timeRange={timeRange}
/>
)}
<EuiSpacer size="s" />

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { combineLatest, debounceTime, skip } from 'rxjs';
import { AnyPublishingSubject, PublishingSubject, UnwrapPublishingSubjectTuple } from './types';
@ -25,6 +25,8 @@ const hasSubjectsArrayChanged = (
};
/**
* @deprecated use useBatchedPublishingSubjects instead.
*
* Batches the latest values of multiple publishing subjects into a single object. Use this to avoid unnecessary re-renders.
* Use when `subjects` may not be defined on initial component render.
*
@ -59,7 +61,7 @@ export const useBatchedOptionalPublishingSubjects = <
/**
* Subscribe to all subjects and update the latest values when any of them change.
*/
useEffect(() => {
useLayoutEffect(() => {
if (!isFirstRender.current) {
setLatestPublishedValues(unwrapPublishingSubjectArray(subjectsToUse));
} else {

View file

@ -18,7 +18,7 @@ import {
SerializedTitles,
initializeTitleManager,
SerializedPanelState,
useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,
initializeStateManager,
titleComparators,
} from '@kbn/presentation-publishing';
@ -257,7 +257,7 @@ export const getLinksEmbeddableFactory = () => {
});
const Component = () => {
const [links, layout] = useBatchedOptionalPublishingSubjects(
const [links, layout] = useBatchedPublishingSubjects(
stateManager.api.links$,
stateManager.api.layout$
);

View file

@ -49,10 +49,10 @@ export const PresentationPanelInternal = <
const dragHandles = useRef<{ [dragHandleKey: string]: HTMLElement | null }>({});
const viewModeSubject = (() => {
const viewModeSubject = useMemo(() => {
if (apiPublishesViewMode(api)) return api.viewMode$;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode$;
})();
}, [api]);
const [
dataLoading,

View file

@ -20,11 +20,9 @@ import {
EuiToolTip,
htmlIdGenerator,
} from '@elastic/eui';
import {
useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { isCompressed } from '../../../../control_group/utils/is_compressed';
import { OptionsListSelection } from '../../../../../common/options_list/options_list_selections';
import { MIN_POPOVER_WIDTH } from '../../../constants';
@ -52,6 +50,7 @@ export const OptionsListControl = ({
loading,
panelTitle,
fieldFormatter,
defaultPanelTitle,
] = useBatchedPublishingSubjects(
stateManager.exclude,
stateManager.existsSelected,
@ -60,11 +59,10 @@ export const OptionsListControl = ({
api.field$,
api.dataLoading$,
api.title$,
api.fieldFormatter
api.fieldFormatter,
api.defaultTitle$ ?? new BehaviorSubject(undefined)
);
const [defaultPanelTitle] = useBatchedOptionalPublishingSubjects(api.defaultTitle$);
const delimiter = useMemo(() => OptionsListStrings.control.getSeparator(field?.type), [field]);
const { hasSelections, selectionDisplayNode, selectedOptionsCount } = useMemo(() => {

View file

@ -29,10 +29,11 @@ import {
EmbeddableApiContext,
apiCanLockHoverActions,
getViewModeSubject,
useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import { ActionExecutionMeta } from '@kbn/ui-actions-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import { BehaviorSubject } from 'rxjs';
import { uiActionsService } from '../services/kibana_services';
import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
import { FiltersNotificationActionApi } from './filters_notification_action';
@ -77,9 +78,9 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
}
}, [api, setDisableEditButton]);
const [dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects(
api.parentApi?.dataViews$,
getViewModeSubject(api ?? undefined)
const [dataViews, parentViewMode] = useBatchedPublishingSubjects(
api.parentApi?.dataViews$ ?? new BehaviorSubject(undefined),
getViewModeSubject(api) ?? new BehaviorSubject(undefined)
);
const showEditButton = !disableEditbutton && parentViewMode === 'edit' && canEditUnifiedSearch;

View file

@ -13,10 +13,7 @@ import type { BehaviorSubject } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/common';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import type { FetchContext } from '@kbn/presentation-publishing';
import {
useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import type { SearchResponseIncompleteWarning } from '@kbn/search-response-warnings/src/types';
import type { DataGridDensity } from '@kbn/unified-data-table';
@ -70,6 +67,10 @@ export function SearchEmbeddableGridComponent({
totalHitCount,
columnsMeta,
grid,
panelTitle,
panelDescription,
savedSearchTitle,
savedSearchDescription,
] = useBatchedPublishingSubjects(
api.dataLoading$,
api.savedSearch$,
@ -81,7 +82,11 @@ export function SearchEmbeddableGridComponent({
stateManager.rows,
stateManager.totalHitCount,
stateManager.columnsMeta,
stateManager.grid
stateManager.grid,
api.title$,
api.description$,
api.defaultTitle$,
api.defaultDescription$
);
// `api.query$` and `api.filters$` are the initial values from the saved search SO (as of now)
@ -90,14 +95,6 @@ export function SearchEmbeddableGridComponent({
const savedSearchQuery = apiQuery;
const savedSearchFilters = apiFilters;
const [panelTitle, panelDescription, savedSearchTitle, savedSearchDescription] =
useBatchedOptionalPublishingSubjects(
api.title$,
api.description$,
api.defaultTitle$,
api.defaultDescription$
);
const isEsql = useMemo(() => isEsqlMode(savedSearch), [savedSearch]);
const sort = useMemo(() => {

View file

@ -17,6 +17,7 @@ import type {
HasSupportedTriggers,
PublishesBlockingError,
PublishesDataLoading,
PublishesDescription,
PublishesSavedObjectId,
PublishesWritableTitle,
PublishesWritableUnifiedSearch,
@ -102,7 +103,8 @@ export type SearchEmbeddableApi = DefaultEmbeddableApi<SearchEmbeddableSerialize
PublishesSavedObjectId &
PublishesDataLoading &
PublishesBlockingError &
PublishesWritableTitle &
Required<PublishesWritableTitle> &
Required<PublishesDescription> &
PublishesSavedSearch &
PublishesWritableDataViews &
PublishesWritableUnifiedSearch &