Skip to content

Commit

Permalink
[8.13] [dashboard] fix console error "Panel not found" when…
Browse files Browse the repository at this point in the history
… linking or unlinking panel to library (#178464) (#178590)

# Backport

This will backport the following commits from `main` to `8.13`:
- [[dashboard] fix console error "Panel not found" when
linking or unlinking panel to library
(#178464)](#178464)

<!--- 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":"2024-03-13T01:00:04Z","message":"[dashboard]
fix console error \"Panel not found\" when linking or unlinking panel to
library (#178464)\n\ncloses
https://github.com/elastic/kibana/issues/178442\r\n\r\nThere is a timing
issue in `getComponentFromEmbeddable`.\r\n1. `EmbeddablePanel` calls
`getComponentFromEmbeddable`.\r\n2. First `getComponentFromEmbeddable`
call starts async operation `await\r\nPromise.all([startServicesPromise,
embeddablePromise])`\r\n3. Props for `EmbeddablePanel` get updated
because dashboard.panels\r\nchanges. `EmbeddablePanel` calls
`getComponentFromEmbeddable` a second\r\ntime because `props.embeddable`
changes.\r\n4. Results of first `getComponentFromEmbeddable` async call
return. At\r\nthis point, panels has changed and no longer contains id
of old\r\nembeddable, but `getComponentFromEmbeddable` tries to
call\r\n`untilEmbeddableLoaded` even though panel no longer
exists.\r\n\r\nPR resolves issue by\r\n1. adding isMounted check after
async action and returning null if\r\ncomponent is no longer mounted
after `startServicesPromise,\r\nembeddablePromise` promises return\r\n2.
Syncronizing `EmbeddablePanel` `useMemo` and
`PresentationPanel`\r\n`useAsync` dependency chain to ensure they are
consistent.\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"49d8083f6e4e35b82708cb3b2ecd60ff20ac3560","branchLabelMapping":{"^v8.14.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Embedding","Team:Presentation","release_note:skip","v8.13.0","project:embeddableRebuild","v8.14.0"],"title":"[dashboard]
fix console error \"Panel not found\" when linking or unlinking panel to
library","number":178464,"url":"https://github.com/elastic/kibana/pull/178464","mergeCommit":{"message":"[dashboard]
fix console error \"Panel not found\" when linking or unlinking panel to
library (#178464)\n\ncloses
https://github.com/elastic/kibana/issues/178442\r\n\r\nThere is a timing
issue in `getComponentFromEmbeddable`.\r\n1. `EmbeddablePanel` calls
`getComponentFromEmbeddable`.\r\n2. First `getComponentFromEmbeddable`
call starts async operation `await\r\nPromise.all([startServicesPromise,
embeddablePromise])`\r\n3. Props for `EmbeddablePanel` get updated
because dashboard.panels\r\nchanges. `EmbeddablePanel` calls
`getComponentFromEmbeddable` a second\r\ntime because `props.embeddable`
changes.\r\n4. Results of first `getComponentFromEmbeddable` async call
return. At\r\nthis point, panels has changed and no longer contains id
of old\r\nembeddable, but `getComponentFromEmbeddable` tries to
call\r\n`untilEmbeddableLoaded` even though panel no longer
exists.\r\n\r\nPR resolves issue by\r\n1. adding isMounted check after
async action and returning null if\r\ncomponent is no longer mounted
after `startServicesPromise,\r\nembeddablePromise` promises return\r\n2.
Syncronizing `EmbeddablePanel` `useMemo` and
`PresentationPanel`\r\n`useAsync` dependency chain to ensure they are
consistent.\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"49d8083f6e4e35b82708cb3b2ecd60ff20ac3560"}},"sourceBranch":"main","suggestedTargetBranches":["8.13"],"targetPullRequestStates":[{"branch":"8.13","label":"v8.13.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.14.0","branchLabelMappingKey":"^v8.14.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/178464","number":178464,"mergeCommit":{"message":"[dashboard]
fix console error \"Panel not found\" when linking or unlinking panel to
library (#178464)\n\ncloses
https://github.com/elastic/kibana/issues/178442\r\n\r\nThere is a timing
issue in `getComponentFromEmbeddable`.\r\n1. `EmbeddablePanel` calls
`getComponentFromEmbeddable`.\r\n2. First `getComponentFromEmbeddable`
call starts async operation `await\r\nPromise.all([startServicesPromise,
embeddablePromise])`\r\n3. Props for `EmbeddablePanel` get updated
because dashboard.panels\r\nchanges. `EmbeddablePanel` calls
`getComponentFromEmbeddable` a second\r\ntime because `props.embeddable`
changes.\r\n4. Results of first `getComponentFromEmbeddable` async call
return. At\r\nthis point, panels has changed and no longer contains id
of old\r\nembeddable, but `getComponentFromEmbeddable` tries to
call\r\n`untilEmbeddableLoaded` even though panel no longer
exists.\r\n\r\nPR resolves issue by\r\n1. adding isMounted check after
async action and returning null if\r\ncomponent is no longer mounted
after `startServicesPromise,\r\nembeddablePromise` promises return\r\n2.
Syncronizing `EmbeddablePanel` `useMemo` and
`PresentationPanel`\r\n`useAsync` dependency chain to ensure they are
consistent.\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"49d8083f6e4e35b82708cb3b2ecd60ff20ac3560"}}]}]
BACKPORT-->

Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
  • Loading branch information
kibanamachine and nreese authored Mar 13, 2024
1 parent 5950c69 commit 6f8e044
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ import { css } from '@emotion/react';
import { PresentationPanel } from '@kbn/presentation-panel-plugin/public';
import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import { isPromise } from '@kbn/std';
import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState, useRef } from 'react';
import { untilPluginStartServicesReady } from '../kibana_services';
import { EmbeddablePanelProps } from './types';

const getComponentFromEmbeddable = async (
embeddable: EmbeddablePanelProps['embeddable']
): Promise<PanelCompatibleComponent> => {
embeddable: EmbeddablePanelProps['embeddable'],
isMounted: () => boolean
): Promise<PanelCompatibleComponent | null> => {
const startServicesPromise = untilPluginStartServicesReady();
const embeddablePromise =
typeof embeddable === 'function' ? embeddable() : Promise.resolve(embeddable);
const [, unwrappedEmbeddable] = await Promise.all([startServicesPromise, embeddablePromise]);
if (!isMounted()) {
return null;
}
if (unwrappedEmbeddable.parent) {
await unwrappedEmbeddable.parent.untilEmbeddableLoaded(unwrappedEmbeddable.id);
}
Expand Down Expand Up @@ -55,9 +59,33 @@ const getComponentFromEmbeddable = async (

/**
* Loads and renders a legacy embeddable.
*
* Ancestry chain must use 'key' attribute to reset DOM and state when embeddable changes
* For example <Parent key={embeddableId}><EmbeddablePanel/></Parent>
*/
export const EmbeddablePanel = (props: EmbeddablePanelProps) => {
// can not use useMountedState
// 1. useMountedState defaults mountedRef to false and sets mountedRef to true in useEffect
// 2. embeddable can be an object or a function that returns a promise
// 3. when embeddable is an object, Promise.resolve(embeddable) returns before
// useMountedState useEffect is called and thus isMounted() returns false when component has not been unmounted
const mountedRef = useRef<boolean>(true);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const isMounted = () => {
return mountedRef.current;
};
const { embeddable, ...passThroughProps } = props;
const componentPromise = useMemo(() => getComponentFromEmbeddable(embeddable), [embeddable]);
const componentPromise = useMemo(
() => getComponentFromEmbeddable(embeddable, isMounted),
// Ancestry chain is expected to use 'key' attribute to reset DOM and state
// when embeddable needs to be re-loaded
// empty array is consistent with PresentationPanel useAsync dependency check
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <PresentationPanel {...passThroughProps} Component={componentPromise} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const PresentationPanel = <
]);
const Panel = panelModule.PresentationPanelInternal;
return { Panel, unwrappedComponent };
// Ancestry chain is expected to use 'key' attribute to reset DOM and state
// when unwrappedComponent needs to be re-loaded
}, []);

if (error || (!loading && (!value?.Panel || !value?.unwrappedComponent))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,5 @@ export type PresentationPanelProps<
ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi,
PropsType extends {} = {}
> = Omit<PresentationPanelInternalProps<ApiType, PropsType>, 'Component'> & {
Component: MaybePromise<PanelCompatibleComponent<ApiType, PropsType>>;
Component: MaybePromise<PanelCompatibleComponent<ApiType, PropsType> | null>;
};

0 comments on commit 6f8e044

Please sign in to comment.