[8.18] [canvas] fix All embeddables rebuilt on refresh (#209677) (#209853)

# Backport

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

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/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":["9.0","8.18","8.16","8.17","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"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,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
This commit is contained in:
Kibana Machine 2025-02-06 06:46:45 +11:00 committed by GitHub
parent f7bb2f4769
commit 4225d4db0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 23 deletions

View file

@ -8,10 +8,18 @@
import { CoreStart } from '@kbn/core/public';
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 {
@ -27,6 +35,8 @@ import { useGetAppContext } from './use_get_app_context';
const { embeddable: strings } = RendererStrings;
const children: Record<string, { setFilters: (filters: Filter[] | undefined) => void }> = {};
const renderReactEmbeddable = ({
type,
uuid,
@ -45,7 +55,14 @@ 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
@ -69,6 +86,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);
}
},
};
}}
/>
);
};
@ -95,24 +128,30 @@ export const embeddableRendererFactory = (
help: strings.getHelpDescription(),
reuseDomNode: true,
render: async (domNode, { input, embeddableType, canvasApi }, handlers) => {
const uniqueId = handlers.getElementId();
ReactDOM.render(
renderReactEmbeddable({
input,
handlers,
uuid: uniqueId,
type: embeddableType,
container: canvasApi,
core,
}),
domNode,
() => handlers.done()
);
const uuid = handlers.getElementId();
const api = children[uuid];
if (!api) {
ReactDOM.render(
renderReactEmbeddable({
input,
handlers,
uuid,
type: embeddableType,
container: canvasApi,
core,
}),
domNode,
() => handlers.done()
);
handlers.onDestroy(() => {
handlers.onEmbeddableDestroyed();
return ReactDOM.unmountComponentAtNode(domNode);
});
handlers.onDestroy(() => {
delete children[uuid];
handlers.onEmbeddableDestroyed();
return ReactDOM.unmountComponentAtNode(domNode);
});
} else {
api.setFilters(input.filters);
}
},
});
};

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;
};