[canvas] fix parent API should implement unified search interface with react embeddables (#183772)

Closes https://github.com/elastic/kibana/issues/183766

### test instructions
1. start kibana with `yarn start --run-examples`
2. install sample web logs data set
3. create new canvas work pad
4. Click "Add element" -> "Filter" -> "Dropdown select"
5. Select "data" tab and configure filter to pull from logs sample data
and `machine.os.keyword` field.
6. Select "display" tab and "Value column" and "Filter column" to
`machine.os.keyword`.
7. Click "Add element" -> "Filter" -> "Time filter"
9. Click "Select type" -> "Search example"
10. Click panel context menu -> "Settings" and turn off custom time
range
11. Click "Expression editor" button in lower right corner to open the
expression editor for the map and prefix the expression with `kibana |
selectFilter | embeddable`
<img width="500" alt="Screenshot 2024-05-17 at 11 50 42 AM"
src="453096d5-c254-4f43-a09c-386586aa05a1">
12. Interact with drop down filter and time range filter. Verify Search
react embeddable updates as expected
This commit is contained in:
Nathan Reese 2024-05-22 11:01:29 -06:00 committed by GitHub
parent a7cb2271e1
commit f8c582da25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 64 additions and 18 deletions

View file

@ -27,10 +27,16 @@ export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => {
embeddable.addNewPanel<SearchSerializedState>(
{
panelType: SEARCH_EMBEDDABLE_ID,
initialState: {},
},
true
);
},
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_SEARCH_ACTION_ID);
if (uiActions.hasTrigger('ADD_CANVAS_ELEMENT_TRIGGER')) {
// Because Canvas is not enabled in Serverless, this trigger might not be registered - only attach
// the create action if the Canvas-specific trigger does indeed exist.
uiActions.attachAction('ADD_CANVAS_ELEMENT_TRIGGER', ADD_SEARCH_ACTION_ID);
}
};

View file

@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
import React, { useEffect, useMemo } from 'react';
import { BehaviorSubject } from 'rxjs';
import React, { useMemo } from 'react';
import { TimeRange } from '@kbn/es-query';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { useSearchApi } from '@kbn/presentation-publishing';
import type { SearchApi, SearchSerializedState } from './types';
import { SEARCH_EMBEDDABLE_ID } from './constants';
@ -28,23 +28,15 @@ export function SearchEmbeddableRenderer(props: Props) {
// only run onMount
}, []);
const parentApi = useMemo(() => {
return {
timeRange$: new BehaviorSubject(props.timeRange),
getSerializedStateForChild: () => initialState,
};
// only run onMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
parentApi.timeRange$.next(props.timeRange);
}, [props.timeRange, parentApi.timeRange$]);
const searchApi = useSearchApi({ timeRange: props.timeRange });
return (
<ReactEmbeddableRenderer<SearchSerializedState, SearchApi>
type={SEARCH_EMBEDDABLE_ID}
getParentApi={() => parentApi}
getParentApi={() => ({
...searchApi,
getSerializedStateForChild: () => initialState,
})}
hidePanelChrome={true}
/>
);

View file

@ -39,6 +39,7 @@ export {
apiPublishesTimeRange,
apiPublishesUnifiedSearch,
apiPublishesWritableUnifiedSearch,
useSearchApi,
type PublishesTimeRange,
type PublishesUnifiedSearch,
type PublishesWritableUnifiedSearch,

View file

@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
import { TimeRange, Filter, Query, AggregateQuery } from '@kbn/es-query';
import { useEffect, useMemo } from 'react';
import { PublishingSubject } from '../../publishing_subject';
export interface PublishesTimeRange {
@ -68,3 +70,40 @@ export const apiPublishesWritableUnifiedSearch = (
typeof (unknownApi as PublishesWritableUnifiedSearch).setQuery === 'function'
);
};
/**
* React hook that converts search props into search observable API
*/
export function useSearchApi({
filters,
query,
timeRange,
}: {
filters?: Filter[];
query?: Query | AggregateQuery;
timeRange?: TimeRange;
}) {
const searchApi = useMemo(() => {
return {
filters$: new BehaviorSubject<Filter[] | undefined>(filters),
query$: new BehaviorSubject<Query | AggregateQuery | undefined>(query),
timeRange$: new BehaviorSubject<TimeRange | undefined>(timeRange),
};
// filters, query, timeRange only used as initial values. Changes do not effect memoized value
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
searchApi.filters$.next(filters);
}, [filters, searchApi.filters$]);
useEffect(() => {
searchApi.query$.next(query);
}, [query, searchApi.query$]);
useEffect(() => {
searchApi.timeRange$.next(timeRange);
}, [timeRange, searchApi.timeRange$]);
return searchApi;
}

View file

@ -17,6 +17,7 @@ import {
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import React, { FC } from 'react';
import ReactDOM from 'react-dom';
import { useSearchApi } from '@kbn/presentation-publishing';
import { omit } from 'lodash';
import { pluginServices } from '../../../public/services';
import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
@ -57,6 +58,7 @@ const renderReactEmbeddable = ({
// wrap in functional component to allow usage of hooks
const RendererWrapper: FC<{}> = () => {
const getAppContext = useGetAppContext(core);
const searchApi = useSearchApi({ filters: input.filters });
return (
<ReactEmbeddableRenderer
@ -66,8 +68,9 @@ const renderReactEmbeddable = ({
...container,
getAppContext,
getSerializedStateForChild: () => ({
rawState: omit(input, 'disableTriggers'),
rawState: omit(input, ['disableTriggers', 'filters']),
}),
...searchApi,
})}
key={`${type}_${uuid}`}
onAnyStateChange={(newState) => {

View file

@ -8,7 +8,12 @@
import type { TimeRange } from '@kbn/es-query';
import { Filter } from '@kbn/es-query';
import { EmbeddableInput as Input } from '@kbn/embeddable-plugin/common';
import { HasAppContext, HasDisableTriggers, PublishesViewMode } from '@kbn/presentation-publishing';
import {
HasAppContext,
HasDisableTriggers,
PublishesViewMode,
PublishesUnifiedSearch,
} from '@kbn/presentation-publishing';
import { CanAddNewPanel, HasSerializedChildState } from '@kbn/presentation-containers';
export type EmbeddableInput = Input & {
@ -21,4 +26,4 @@ export type CanvasContainerApi = PublishesViewMode &
CanAddNewPanel &
HasDisableTriggers &
HasSerializedChildState &
Partial<HasAppContext>;
Partial<HasAppContext & PublishesUnifiedSearch>;