/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { useEffect, useState } from 'react'; import { EmbeddableInput, IEmbeddable } from './i_embeddable'; import { EmbeddableRoot } from './embeddable_root'; import { EmbeddableFactory } from './embeddable_factory'; import { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; /** * This type is a publicly exposed props of {@link EmbeddableRenderer} * Union is used to validate that or factory or embeddable is passed in, but it can't be both simultaneously * In case when embeddable is passed in, input is optional, because there is already an input inside of embeddable object * In case when factory is used, then input is required, because it will be used as initial input to create an embeddable object */ export type EmbeddableRendererProps = | EmbeddableRendererPropsWithEmbeddable | EmbeddableRendererWithFactory; interface EmbeddableRendererPropsWithEmbeddable { input?: I; onInputUpdated?: (newInput: I) => void; embeddable: IEmbeddable; } function isWithEmbeddable( props: EmbeddableRendererProps ): props is EmbeddableRendererPropsWithEmbeddable { return 'embeddable' in props; } interface EmbeddableRendererWithFactory { input: I; onInputUpdated?: (newInput: I) => void; factory: EmbeddableFactory; } function isWithFactory( props: EmbeddableRendererProps ): props is EmbeddableRendererWithFactory { return 'factory' in props; } /** * Helper react component to render an embeddable * Can be used if you have an embeddable object or an embeddable factory * Supports updating input by passing `input` prop * * @remarks * This component shouldn't be used inside an embeddable container to render embeddable children * because children may lose inherited input, here is why: * * When passing `input` inside a prop, internally there is a call: * * ```ts * embeddable.updateInput(input); * ``` * If you are simply rendering an embeddable, it's no problem. * * However when you are dealing with containers, * you want to be sure to only pass into updateInput the actual state that changed. * This is because calling child.updateInput({ foo }) will make foo explicit state. * It cannot be inherited from it's parent. * * For example, on a dashboard, the time range is inherited by all children, * unless they had their time range set explicitly. * This is how "per panel time range" works. * That action calls embeddable.updateInput({ timeRange }), * and the time range will no longer be inherited from the container. * * see: https://github.com/elastic/kibana/pull/67783#discussion_r435447657 for more details. * refer to: examples/embeddable_explorer for examples with correct usage of this component. * * @public * @param props - {@link EmbeddableRendererProps} */ export const EmbeddableRenderer = ( props: EmbeddableRendererProps ) => { const { input, onInputUpdated } = props; const [embeddable, setEmbeddable] = useState | ErrorEmbeddable | undefined>( isWithEmbeddable(props) ? props.embeddable : undefined ); const [loading, setLoading] = useState(!isWithEmbeddable(props)); const [error, setError] = useState(); const latestInput = React.useRef(props.input); useEffect(() => { latestInput.current = input; }, [input]); const factoryFromProps = isWithFactory(props) ? props.factory : undefined; const embeddableFromProps = isWithEmbeddable(props) ? props.embeddable : undefined; useEffect(() => { let canceled = false; if (embeddableFromProps) { setEmbeddable(embeddableFromProps); return; } // keeping track of embeddables created by this component to be able to destroy them let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined; if (factoryFromProps) { setEmbeddable(undefined); setLoading(true); factoryFromProps .create(latestInput.current!) .then((createdEmbeddable) => { if (canceled) { if (createdEmbeddable) { createdEmbeddable.destroy(); } } else { createdEmbeddableRef = createdEmbeddable; setEmbeddable(createdEmbeddable); } }) .catch((err) => { if (canceled) return; setError(err?.message); }) .finally(() => { if (canceled) return; setLoading(false); }); } return () => { canceled = true; if (createdEmbeddableRef) { createdEmbeddableRef.destroy(); } }; }, [factoryFromProps, embeddableFromProps]); useEffect(() => { if (!embeddable) return; if (isErrorEmbeddable(embeddable)) return; if (!onInputUpdated) return; const sub = embeddable.getInput$().subscribe((newInput) => { onInputUpdated(newInput); }); return () => { sub.unsubscribe(); }; }, [embeddable, onInputUpdated]); return ; };