[8.16] [canvas] fix All embeddables rebuilt on refresh (#209677) (#209862)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[canvas] fix All embeddables rebuilt on refresh
(#209677)](https://github.com/elastic/kibana/pull/209677)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Nathan
Reese","email":"reese.nathan@elastic.co"},"sourceCommit":{"committedDate":"2025-02-05T17:34:44Z","message":"[canvas]
fix All embeddables rebuilt on refresh (#209677)\n\nFixes
https://github.com/elastic/kibana/issues/209566\r\n\r\n###
Problem\r\nAny input change causes Canvas embeddable's to get
re-created. This\r\nmeans that setting a filter control or clicking the
refresh button\r\ncauses embeddables to get re-created.\r\n\r\nIn the
old embeddable system, the Canvas would only
call\r\n`embeddable.updateInput` and `embeddable.reload` on
[input\r\nchanges](https://github.com/elastic/kibana/blob/8.13/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx#L163).\r\n\r\n###
Solution\r\nPR updates embeddable renderer to store embeddable API.
Then, on input\r\nchanges, Canvas calls
`embeddable.setFilters`.\r\n\r\nThere is no `embeddable.updateInput`
equivalent in the new embeddable\r\nsystem. Instead, each state key
needs to be updated by a setter.
The\r\n[Canvas\r\ndocumentation](https://www.elastic.co/guide/en/kibana/current/canvas-function-reference.html#embeddable_fn)\r\nstates
that the embeddable function only accepts `filters`. Therefore,\r\nthe
only key that is expected to change from the input is
`filters`.\r\nPlease correct me if this is an incorrect
assumption.\r\n\r\n### Test instructions\r\n1) install sample web
logs\r\n2) install canvas saved object and reload kibana (otherwise
canvas is\r\nnot available in the nav menu)\r\n3) open new canvas\r\n4)
add map embeddable\r\n5) add filter control. set source to sample web
logs and field to\r\n`geo.dest`.\r\n<img width=\"200\" alt=\"Screenshot
2025-02-04 at 2 58
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/6862f0bc-4f61-4f16-aa7c-ea8008cfdbf9\"\r\n/>\r\n6)
prefix map element expression with `kibana | selectFilter` so
it\r\nlooks like `kibana | selectFilter | embeddable config=...`\r\n7)
change filter. Verify map updates but map embeddable is
not\r\nre-created.\r\n8) click refresh button, Verify map updates but is
not re-created.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"fe9023efffc2671cec0597b14950cc2a204e7ade","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Presentation","v9.0.0","backport:version","v8.18.0","v8.16.4","v8.17.2","v9.1.0","v8.19.0"],"title":"[canvas]
fix All embeddables rebuilt on
refresh","number":209677,"url":"https://github.com/elastic/kibana/pull/209677","mergeCommit":{"message":"[canvas]
fix All embeddables rebuilt on refresh (#209677)\n\nFixes
https://github.com/elastic/kibana/issues/209566\r\n\r\n###
Problem\r\nAny input change causes Canvas embeddable's to get
re-created. This\r\nmeans that setting a filter control or clicking the
refresh button\r\ncauses embeddables to get re-created.\r\n\r\nIn the
old embeddable system, the Canvas would only
call\r\n`embeddable.updateInput` and `embeddable.reload` on
[input\r\nchanges](https://github.com/elastic/kibana/blob/8.13/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx#L163).\r\n\r\n###
Solution\r\nPR updates embeddable renderer to store embeddable API.
Then, on input\r\nchanges, Canvas calls
`embeddable.setFilters`.\r\n\r\nThere is no `embeddable.updateInput`
equivalent in the new embeddable\r\nsystem. Instead, each state key
needs to be updated by a setter.
The\r\n[Canvas\r\ndocumentation](https://www.elastic.co/guide/en/kibana/current/canvas-function-reference.html#embeddable_fn)\r\nstates
that the embeddable function only accepts `filters`. Therefore,\r\nthe
only key that is expected to change from the input is
`filters`.\r\nPlease correct me if this is an incorrect
assumption.\r\n\r\n### Test instructions\r\n1) install sample web
logs\r\n2) install canvas saved object and reload kibana (otherwise
canvas is\r\nnot available in the nav menu)\r\n3) open new canvas\r\n4)
add map embeddable\r\n5) add filter control. set source to sample web
logs and field to\r\n`geo.dest`.\r\n<img width=\"200\" alt=\"Screenshot
2025-02-04 at 2 58
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/6862f0bc-4f61-4f16-aa7c-ea8008cfdbf9\"\r\n/>\r\n6)
prefix map element expression with `kibana | selectFilter` so
it\r\nlooks like `kibana | selectFilter | embeddable config=...`\r\n7)
change filter. Verify map updates but map embeddable is
not\r\nre-created.\r\n8) click refresh button, Verify map updates but is
not re-created.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"fe9023efffc2671cec0597b14950cc2a204e7ade"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.17"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/209855","number":209855,"state":"OPEN"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/209853","number":209853,"state":"OPEN"},{"branch":"8.16","label":"v8.16.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209677","number":209677,"mergeCommit":{"message":"[canvas]
fix All embeddables rebuilt on refresh (#209677)\n\nFixes
https://github.com/elastic/kibana/issues/209566\r\n\r\n###
Problem\r\nAny input change causes Canvas embeddable's to get
re-created. This\r\nmeans that setting a filter control or clicking the
refresh button\r\ncauses embeddables to get re-created.\r\n\r\nIn the
old embeddable system, the Canvas would only
call\r\n`embeddable.updateInput` and `embeddable.reload` on
[input\r\nchanges](https://github.com/elastic/kibana/blob/8.13/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx#L163).\r\n\r\n###
Solution\r\nPR updates embeddable renderer to store embeddable API.
Then, on input\r\nchanges, Canvas calls
`embeddable.setFilters`.\r\n\r\nThere is no `embeddable.updateInput`
equivalent in the new embeddable\r\nsystem. Instead, each state key
needs to be updated by a setter.
The\r\n[Canvas\r\ndocumentation](https://www.elastic.co/guide/en/kibana/current/canvas-function-reference.html#embeddable_fn)\r\nstates
that the embeddable function only accepts `filters`. Therefore,\r\nthe
only key that is expected to change from the input is
`filters`.\r\nPlease correct me if this is an incorrect
assumption.\r\n\r\n### Test instructions\r\n1) install sample web
logs\r\n2) install canvas saved object and reload kibana (otherwise
canvas is\r\nnot available in the nav menu)\r\n3) open new canvas\r\n4)
add map embeddable\r\n5) add filter control. set source to sample web
logs and field to\r\n`geo.dest`.\r\n<img width=\"200\" alt=\"Screenshot
2025-02-04 at 2 58
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/6862f0bc-4f61-4f16-aa7c-ea8008cfdbf9\"\r\n/>\r\n6)
prefix map element expression with `kibana | selectFilter` so
it\r\nlooks like `kibana | selectFilter | embeddable config=...`\r\n7)
change filter. Verify map updates but map embeddable is
not\r\nre-created.\r\n8) click refresh button, Verify map updates but is
not re-created.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"fe9023efffc2671cec0597b14950cc2a204e7ade"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/209854","number":209854,"state":"OPEN"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2025-02-05 14:33:25 -07:00 committed by GitHub
parent baf44fa969
commit 7bd84beb05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 80 additions and 26 deletions

View file

@ -35,6 +35,7 @@ export {
initializeTimeRange,
type SerializedTimeRange,
} from './interfaces/fetch/initialize_time_range';
export { apiPublishesReload, type PublishesReload } from './interfaces/fetch/publishes_reload';
export {
apiPublishesFilters,
apiPublishesPartialUnifiedSearch,

View file

@ -15,10 +15,18 @@ import {
ReactEmbeddableRenderer,
} from '@kbn/embeddable-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { useSearchApi } from '@kbn/presentation-publishing';
import { omit } from 'lodash';
import {
AggregateQuery,
COMPARE_ALL_OPTIONS,
Filter,
Query,
TimeRange,
onlyDisabledFiltersChanged,
} from '@kbn/es-query';
import { BehaviorSubject } from 'rxjs';
import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
import { RendererStrings } from '../../../i18n';
import {
@ -35,11 +43,13 @@ import { embeddableService } from '../../../public/services/kibana_services';
const { embeddable: strings } = RendererStrings;
// registry of references to embeddables on the workpad
// registry of references to legacy embeddables on the workpad
const embeddablesRegistry: {
[key: string]: IEmbeddable | Promise<IEmbeddable>;
} = {};
const children: Record<string, { setFilters: (filters: Filter[] | undefined) => void }> = {};
const renderReactEmbeddable = ({
type,
uuid,
@ -58,7 +68,13 @@ const renderReactEmbeddable = ({
// wrap in functional component to allow usage of hooks
const RendererWrapper: FC<{}> = () => {
const getAppContext = useGetAppContext(core);
const searchApi = useSearchApi({ filters: input.filters });
const searchApi = useMemo(() => {
return {
filters$: new BehaviorSubject<Filter[] | undefined>(input.filters),
query$: new BehaviorSubject<Query | AggregateQuery | undefined>(undefined),
timeRange$: new BehaviorSubject<TimeRange | undefined>(undefined),
};
}, []);
return (
<ReactEmbeddableRenderer
@ -82,6 +98,22 @@ const renderReactEmbeddable = ({
);
if (newExpression) handlers.onEmbeddableInputChange(newExpression);
}}
onApiAvailable={(api) => {
children[uuid] = {
...api,
setFilters: (filters: Filter[] | undefined) => {
if (
!onlyDisabledFiltersChanged(searchApi.filters$.getValue(), filters, {
...COMPARE_ALL_OPTIONS,
// do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results)
state: false,
})
) {
searchApi.filters$.next(filters);
}
},
};
}}
/>
);
};
@ -138,26 +170,32 @@ export const embeddableRendererFactory = (
);
if (embeddableService.reactEmbeddableRegistryHasKey(embeddableType)) {
/**
* Prioritize React embeddables
*/
ReactDOM.render(
renderReactEmbeddable({
input,
handlers,
uuid: uniqueId,
type: embeddableType,
container: canvasApi,
core,
}),
domNode,
() => handlers.done()
);
const api = children[uniqueId];
if (!api) {
/**
* Prioritize React embeddables
*/
ReactDOM.render(
renderReactEmbeddable({
input,
handlers,
uuid: uniqueId,
type: embeddableType,
container: canvasApi,
core,
}),
domNode,
() => handlers.done()
);
handlers.onDestroy(() => {
handlers.onEmbeddableDestroyed();
return ReactDOM.unmountComponentAtNode(domNode);
});
handlers.onDestroy(() => {
delete children[uniqueId];
handlers.onEmbeddableDestroyed();
return ReactDOM.unmountComponentAtNode(domNode);
});
} else {
api.setFilters(input.filters);
}
} else if (!embeddablesRegistry[uniqueId]) {
/**
* Handle legacy embeddables - embeddable does not exist in registry

View file

@ -7,7 +7,7 @@
import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { EmbeddableInput } from '@kbn/embeddable-plugin/common';
import { ViewMode } from '@kbn/presentation-publishing';
@ -19,6 +19,8 @@ import { METRIC_TYPE, trackCanvasUiMetric } from '../../lib/ui_metric';
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
const reload$ = new Subject<void>();
export const useCanvasApi: () => CanvasContainerApi = () => {
const selectedPageId = useSelector(getSelectedPage);
const dispatch = useDispatch();
@ -38,6 +40,10 @@ export const useCanvasApi: () => CanvasContainerApi = () => {
const getCanvasApi = useCallback((): CanvasContainerApi => {
return {
reload$,
reload: () => {
reload$.next();
},
viewMode: new BehaviorSubject<ViewMode>('edit'), // always in edit mode
addNewPanel: async ({
panelType,

View file

@ -15,6 +15,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { fetchAllRenderables } from '../../../state/actions/elements';
import { getInFlight } from '../../../state/selectors/resolved_args';
import { ToolTipShortcut } from '../../tool_tip_shortcut';
import { useCanvasApi } from '../../hooks/use_canvas_api';
const strings = {
getRefreshAriaLabel: () =>
@ -30,7 +31,11 @@ const strings = {
export const RefreshControl = () => {
const dispatch = useDispatch();
const inFlight = useSelector(getInFlight);
const doRefresh = useCallback(() => dispatch(fetchAllRenderables()), [dispatch]);
const canvasApi = useCanvasApi();
const doRefresh = useCallback(() => {
canvasApi.reload();
dispatch(fetchAllRenderables());
}, [canvasApi, dispatch]);
return (
<EuiToolTip

View file

@ -12,6 +12,7 @@ import type {
HasAppContext,
HasDisableTriggers,
HasType,
PublishesReload,
PublishesViewMode,
PublishesUnifiedSearch,
} from '@kbn/presentation-publishing';
@ -28,4 +29,7 @@ export type CanvasContainerApi = PublishesViewMode &
HasDisableTriggers &
HasType &
HasSerializedChildState &
Partial<HasAppContext & PublishesUnifiedSearch>;
PublishesReload &
Partial<HasAppContext & PublishesUnifiedSearch> & {
reload: () => void;
};