Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Data Explorer] Understand the setup and start workflow between Data Explorer and Discover View #4419

Closed
ananzh opened this issue Jun 28, 2023 · 0 comments
Assignees
Labels
data explorer Issues related to the Data Explorer project

Comments

@ananzh
Copy link
Member

ananzh commented Jun 28, 2023

Description

DataExplorerPlugin and DiscoverPlugin are both independent plugins that are meant to work together. The DataExplorerPlugin is designed to handle the rendering of the application, and it depends on some services from DiscoverPlugin. For example, to render table in createCanvas from renderApp in DataExplorerPlugin, we need DiscoverServices to be built and initialized.

Currently the createCanvas function is indeed being called only once when the Discover plugin is being set up. The return value of this function (a React Element) is then saved in the ui object of the registered view and this same React Element is used every time the DataExplorerApp is rendered. Because React Elements are plain objects and not functions, they don't get re-evaluated or re-rendered unless their props or state change, which isn't the case here. What we want is to conditionally-render/re-render createCanvas based on the initialization of Discover services. However, these services within the DiscoverPlugin are initialized asynchronously, meaning they may not be ready when the DataExplorerPlugin mount.

The problem is how to get the createCanvas function to re-render once the services in DiscoverPlugin have been initialized, as merely updating the services with setServices(services) is insufficient to trigger a re-render, because services aren't being tracked as a state or prop within a React component.

Approach 1 The Observable Method and Its Challenges

The Observable approach involves DiscoverPlugin exposing an Observable that emits values reflecting the initialization state of the services. The DataExplorerPlugin would then subscribe to this Observable and conditionally render content based on the emitted values.

I have attempted to implement this by making DiscoverPlugin expose an Observable servicesInitialized$ as part of its start contract. DataExplorerPlugin then subscribes to this Observable and includes it in the services passed to createCanvas through OpenSearchDashboardsContextProvider. Here is some implementation details:

  • emit true after Discover initialize the service
start(core: CoreStart, plugins: DiscoverStartPlugins) {
    setUiActions(plugins.uiActions);

    this.initializeServices = async () => {
      if (this.servicesInitialized) {
        return { core, plugins };
      }
      const services = await buildServices(core, plugins, this.initializerContext);
      setServices(services);
      this.servicesInitialized$.next(true);

      return { core, plugins };
    };

    return {
      servicesInitialized$: this.servicesInitialized$.asObservable(),
      urlGenerator: this.urlGenerator,
      savedSearchLoader: createSavedSearchesLoader({
        savedObjectsClient: core.savedObjects.client,
        indexPatterns: plugins.data.indexPatterns,
        search: plugins.data.search,
        chrome: core.chrome,
        overlays: core.overlays,
      }),
    };
  }
  • mount is relies on DiscoverStart and services object passing to renderApp subscribe to servicesInitialized
async mount(params: AppMountParameters, plugins:DataExplorerPluginSetupDependencies ) {
        const [coreStart, depsStart] = await core.getStartServices();

        const services: DataExplorerServices = {
          ...coreStart,
          viewRegistry: viewService.start(),
          discoverServicesInitialized$: plugins.discover.servicesInitialized$,
        };

        // Load application bundle
        const { renderApp } = await import('./application');
        // Get start services as specified in opensearch_dashboards.json
        // Render the application
        return renderApp(coreStart, services, params);
      },
  • when render createCanvas, it will call useOpenSearchDashboards() to fetch services and discoverServicesInitialized$ will trigger setDiscoverServicesInitialized to update discoverServicesInitialized and will render new service
import React from 'react';
import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public';

export const createCanvas = () => {
  const services = useOpenSearchDashboards();
  const [discoverServicesInitialized, setDiscoverServicesInitialized] = React.useState(false);

  React.useEffect(() => {
    let subscription: any;
    if(services?.discoverServicesInitialized$) {
      subscription = services.discoverServicesInitialized$.subscribe(setDiscoverServicesInitialized);
    }
    return () => subscription?.unsubscribe();
  }, [services]);

  if (!discoverServicesInitialized) {
    return <div>Test Canvas</div>;
  }
  return <div>Test Canvas Has Services</div>;
};

However, this method proved to be complex and non-functional for several reasons:

  • There's a challenge in passing the DiscoverStart contract to the mount function of the DataExplorerPlugin. OpenSearch Dashboards doesn't allow passing additional dependencies to the mount function directly.
  • DataExplorerPlugin should setup and start before DiscoverPlugin, meaning DiscoverPlugin's start contract might not be available when DataExplorerPlugin's mount method is called.

The Observable servicesInitialized$ was not triggering a re-render of the createCanvas function as expected, possibly because the Observable itself was not triggering a change in the React component state or props.

Approach 2 Using Shared State (Redux)

Some rough thoughts:

  • DataExplorerPlugin sets up a Redux store during its setup method. The store would hold the state of whether the services are initialized or not. Initially, this state would be false.

  • The DiscoverPlugin then receives a function to dispatch a servicesInitialized action (and a selector to read the state as part of its setup or start contract.

  • When the services in DiscoverPlugin finish initializing, it would dispatch the servicesInitialized action, setting the shared state to true.

  • The createCanvas function can then be connected to this shared state. When the state changes, it would trigger a re-render.

Option 3 Lazy load (selected)

Use React.lazy to load a component in createCanvas. In this case, React will render the fallback component specified inside Suspense. Once the import (or other async operation) completes and the component becomes available, React will render the actual component in place of the fallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
data explorer Issues related to the Data Explorer project
Projects
None yet
Development

No branches or pull requests

2 participants