Skip to content

Commit

Permalink
Mount views dynamically with services (opensearch-project#4519)
Browse files Browse the repository at this point in the history
Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>
  • Loading branch information
ashwin-pc committed Jul 10, 2023
1 parent d4acb33 commit d4ce533
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 61 deletions.
25 changes: 3 additions & 22 deletions src/plugins/data_explorer/public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
*/

import React from 'react';
import { EuiPageTemplate } from '@elastic/eui';

import { CoreStart, ScopedHistory } from '../../../../core/public';
import { Sidebar } from './sidebar';
import { useView } from '../utils/use';
import { NoView } from './no_view';
import { AppContainer } from './app_container';

interface DataExplorerAppDeps {
basename: string;
Expand All @@ -18,24 +15,8 @@ interface DataExplorerAppDeps {
history: ScopedHistory;
}

export const DataExplorerApp = ({ basename, history }: DataExplorerAppDeps) => {
export const DataExplorerApp = (deps: DataExplorerAppDeps) => {
const { view } = useView();

if (!view) {
return <NoView />;
}

// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
return (
<EuiPageTemplate
pageSideBar={<Sidebar />}
className="dePageTemplate"
template="default"
restrictWidth={false}
paddingSize="none"
>
{view.ui.canvas}
</EuiPageTemplate>
);
return <AppContainer view={view} />;
};
47 changes: 47 additions & 0 deletions src/plugins/data_explorer/public/components/app_container.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { AppContainer } from './app_container';
import { render } from '@testing-library/react';
import { View } from '../services/view_service/view';
import { ViewMountParameters } from '../services/view_service';

describe('DataExplorerApp', () => {
const createView = () => {
return new View({
id: 'test-view',
title: 'Test View',
defaultPath: '/test-path',
appExtentions: {} as any,
mount: async ({ canvasElement, panelElement }: ViewMountParameters) => {
const canvasContent = document.createElement('div');
const panelContent = document.createElement('div');
canvasContent.innerHTML = 'canvas-content';
panelContent.innerHTML = 'panel-content';
canvasElement.appendChild(canvasContent);
panelElement.appendChild(panelContent);
return () => {
canvasContent.remove();
panelContent.remove();
};
},
});
};

it('should render NoView when a non existent view is selected', () => {
const { container } = render(<AppContainer />);

expect(container).toContainHTML('View not found');
});

// TODO: Complete once state management is in place
// it('should render the canvas and panel when selected', () => {
// const view = createView();
// const { container } = render(<AppContainer view={view} />);

// expect(container).toMatchSnapshot();
// });
});
83 changes: 83 additions & 0 deletions src/plugins/data_explorer/public/components/app_container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useLayoutEffect, useRef, useState } from 'react';
import { EuiPageTemplate } from '@elastic/eui';

import { Sidebar } from './sidebar';
import { NoView } from './no_view';
import { View } from '../services/view_service/view';

export const AppContainer = ({ view }: { view?: View }) => {
const [showSpinner, setShowSpinner] = useState(false);
const canvasRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const unmountRef = useRef<any>(null);

useLayoutEffect(() => {
const unmount = () => {
if (unmountRef.current) {
unmountRef.current();
unmountRef.current = null;
}
};

if (!view) {
return;
}

// unmount the previous view
unmount();

const mount = async () => {
setShowSpinner(true);
try {
unmountRef.current =
(await view.mount({
canvasElement: canvasRef.current!,
panelElement: panelRef.current!,
})) || null;
} catch (e) {
// TODO: add error UI
// eslint-disable-next-line no-console
console.error(e);
} finally {
// if (canvasRef.current && panelRef.current) {
if (canvasRef.current) {
setShowSpinner(false);
}
}
};

mount();

return unmount;
}, [view]);

// TODO: Make this more robust.
if (!view) {
return <NoView />;
}

// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
return (
<EuiPageTemplate
pageSideBar={
<Sidebar>
<div ref={panelRef} />
</Sidebar>
}
className="dePageTemplate"
template="default"
restrictWidth={false}
paddingSize="none"
>
{/* TODO: improve loading state */}
{showSpinner && <div>Loading...</div>}
<div key={view.id} ref={canvasRef} />
</EuiPageTemplate>
);
};
6 changes: 3 additions & 3 deletions src/plugins/data_explorer/public/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useMemo } from 'react';
import React, { useMemo, FC } from 'react';
import { EuiPanel, EuiComboBox, EuiSelect, EuiSelectOption } from '@elastic/eui';
import { useView } from '../utils/use';

export const Sidebar = () => {
export const Sidebar: FC = ({ children }) => {
const { view, viewRegistry } = useView();
const views = viewRegistry.all();
const viewOptions: EuiSelectOption[] = useMemo(
Expand All @@ -34,7 +34,7 @@ export const Sidebar = () => {
/>
<EuiSelect options={viewOptions} value={view?.id} />
</EuiPanel>
{view?.ui.panel}
{children}
</>
);
};
1 change: 1 addition & 0 deletions src/plugins/data_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export function plugin() {
return new DataExplorerPlugin();
}
export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types';
export { ViewMountParameters, ViewDefinition } from './services/view_service';
11 changes: 7 additions & 4 deletions src/plugins/data_explorer/public/services/view_service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { ReactElement } from 'react';

// TODO: Correctly type this file.

Expand All @@ -11,15 +10,19 @@ interface ViewListItem {
label: string;
}

export interface ViewMountParameters {
canvasElement: HTMLDivElement;
panelElement: HTMLDivElement;
}

export interface ViewDefinition<T = any> {
readonly id: string;
readonly title: string;
readonly ui: {
panel: ReactElement;
canvas: ReactElement;
readonly ui?: {
defaults: T;
reducer: (state: T, action: any) => T;
};
readonly mount: (params: ViewMountParameters) => Promise<() => void>;
readonly defaultPath: string;
readonly appExtentions: {
savedObject: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class View implements IView {
public readonly defaultPath: string;
public readonly appExtentions: IView['appExtentions'];
readonly shouldShow?: (state: any) => boolean;
readonly mount: IView['mount'];

constructor(options: ViewDefinition) {
this.id = options.id;
Expand All @@ -21,5 +22,6 @@ export class View implements IView {
this.defaultPath = options.defaultPath;
this.appExtentions = options.appExtentions;
this.shouldShow = options.shouldShow;
this.mount = options.mount;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { ViewMountParameters } from '../../../../../data_explorer/public';
import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public';
import { DiscoverServices } from '../../../build_services';

export const renderCanvas = (
{ canvasElement }: ViewMountParameters,
services: DiscoverServices
) => {
ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
{/* This is dummy code, inline styles will not be added in production */}
<div style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(services.capabilities.navLinks, null, 2)}
</div>
</OpenSearchDashboardsContextProvider>,
canvasElement
);

return () => ReactDOM.unmountComponentAtNode(canvasElement);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* SPDX-License-Identifier: Apache-2.0
*/

export * from './create_canvas';
export * from './canvas';
export * from './panel';

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { ViewMountParameters } from '../../../../../data_explorer/public';
import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public';
import { DiscoverServices } from '../../../build_services';

export const renderPanel = ({ panelElement }: ViewMountParameters, services: DiscoverServices) => {
ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
<div>Side panel</div>
</OpenSearchDashboardsContextProvider>,
panelElement
);

return () => ReactDOM.unmountComponentAtNode(panelElement);
};
15 changes: 10 additions & 5 deletions src/plugins/discover/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ import {
import { DISCOVER_LEGACY_TOGGLE, PLUGIN_ID } from '../common';
import { DataExplorerPluginSetup, ViewRedirectParams } from '../../data_explorer/public';
import { registerFeature } from './register_feature';
import { createCanvas } from './application/view_components/canvas';
import { createPanel } from './application/view_components/panel';

declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
Expand All @@ -105,7 +103,6 @@ export interface DiscoverSetup {
docViews: {
/**
* Add new doc view shown along with table view and json view in the details of each document in Discover.
* Both react and angular doc views are supported.
* @param docViewRaw
*/
addDocView(docViewRaw: DocViewInput | DocViewInputFn): void;
Expand Down Expand Up @@ -389,12 +386,20 @@ export class DiscoverPlugin
},
},
ui: {
canvas: createCanvas(),
panel: createPanel(),
defaults: {},
reducer: () => ({}),
},
shouldShow: () => true,
mount: async (params) => {
const { renderCanvas, renderPanel } = await import('./application/view_components');
const [coreStart, pluginsStart] = await core.getStartServices();
const services = await buildServices(coreStart, pluginsStart, this.initializerContext);

renderCanvas(params, services);
renderPanel(params, services);

return () => {};
},
});

// this.registerEmbeddable(core, plugins);
Expand Down

0 comments on commit d4ce533

Please sign in to comment.