Skip to content

Commit

Permalink
New core plugin for dynamic content rendering(#7201)
Browse files Browse the repository at this point in the history
Signed-off-by: SuZhou-Joe <[email protected]>
  • Loading branch information
SuZhou-Joe committed Jul 17, 2024
1 parent 0ca967c commit d73444b
Show file tree
Hide file tree
Showing 27 changed files with 1,229 additions and 8 deletions.
9 changes: 9 additions & 0 deletions src/plugins/content_management/opensearch_dashboards.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "contentManagement",
"version": "opensearchDashboards",
"server": false,
"ui": true,
"requiredPlugins": ["embeddable"],
"optionalPlugins": [],
"requiredBundles": ["embeddable"]
}
64 changes: 64 additions & 0 deletions src/plugins/content_management/public/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { I18nProvider } from '@osd/i18n/react';

import {
AppMountParameters,
CoreStart,
SavedObjectsClientContract,
} from 'opensearch-dashboards/public';
import { PageRender } from './components/page_render';
import { Page } from './services';
import { ContentManagementPluginStartDependencies } from './types';
import { EmbeddableStart } from '../../embeddable/public';

interface Props {
params: AppMountParameters;
pages: Page[];
coreStart: CoreStart;
depsStart: ContentManagementPluginStartDependencies;
}

export const renderPage = ({
page,
embeddable,
savedObjectsClient,
}: {
page: Page;
embeddable: EmbeddableStart;
savedObjectsClient: SavedObjectsClientContract;
}) => {
return <PageRender page={page} embeddable={embeddable} savedObjectsClient={savedObjectsClient} />;
};

export const renderApp = (
{ params, pages, coreStart, depsStart }: Props,
element: AppMountParameters['element']
) => {
ReactDOM.render(
<I18nProvider>
<Router history={params.history}>
<Switch>
{pages.map((page) => (
<Route path={[`/${page.config.id}`]}>
{renderPage({
page,
embeddable: depsStart.embeddable,
savedObjectsClient: coreStart.savedObjects.client,
})}
</Route>
))}
</Switch>
</Router>
</I18nProvider>,
element
);

return () => ReactDOM.unmountComponentAtNode(element);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { Container, ContainerInput, EmbeddableStart } from '../../../../embeddable/public';
import { CardList } from './card_list';

export const CARD_CONTAINER = 'CARD_CONTAINER';

export type CardContainerInput = ContainerInput<{ description: string; onClick?: () => void }>;

export class CardContainer extends Container<{}, ContainerInput> {
public readonly type = CARD_CONTAINER;
private node?: HTMLElement;

constructor(input: ContainerInput, private embeddableServices: EmbeddableStart) {
super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory);
}

getInheritedInput() {
return {
viewMode: this.input.viewMode,
};
}

public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(
<CardList embeddable={this} embeddableServices={this.embeddableServices} />,
node
);
}

public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';

import {
EmbeddableFactoryDefinition,
ContainerInput,
EmbeddableStart,
EmbeddableFactory,
ContainerOutput,
} from '../../../../embeddable/public';
import { CARD_CONTAINER, CardContainer } from './card_container';

interface StartServices {
embeddableServices: EmbeddableStart;
}

export type CardContainerFactory = EmbeddableFactory<ContainerInput, ContainerOutput>;
export class CardContainerFactoryDefinition
implements EmbeddableFactoryDefinition<ContainerInput, ContainerOutput> {
public readonly type = CARD_CONTAINER;
public readonly isContainerType = true;

constructor(private getStartServices: () => Promise<StartServices>) {}

public async isEditable() {
return true;
}

public create = async (initialInput: ContainerInput) => {
const { embeddableServices } = await this.getStartServices();
return new CardContainer(initialInput, embeddableServices);
};

public getDisplayName() {
return i18n.translate('contentManagement.cardContainer.displayName', {
defaultMessage: 'Card container',
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public';
import { EuiCard } from '@elastic/eui';

Check failure on line 9 in src/plugins/content_management/public/components/card_container/card_embeddable.tsx

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

`@elastic/eui` import should occur before import of `../../../../embeddable/public`

export const CARD_EMBEDDABLE = 'card_embeddable';
export type CardEmbeddableInput = EmbeddableInput & { description: string; onClick?: () => void };

export class CardEmbeddable extends Embeddable<CardEmbeddableInput> {
public readonly type = CARD_EMBEDDABLE;
private node: HTMLElement | null = null;

constructor(initialInput: CardEmbeddableInput, parent?: IContainer) {
super(initialInput, {}, parent);
}

public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(
<EuiCard
title={this.input.title ?? ''}
description={this.input.description}
display="plain"
onClick={this.input.onClick}
/>,
node
);
}

public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}

public reload() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { EmbeddableFactoryDefinition, IContainer } from '../../../../embeddable/public';
import { CARD_EMBEDDABLE, CardEmbeddable, CardEmbeddableInput } from './card_embeddable';

export class CardEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
public readonly type = CARD_EMBEDDABLE;

public async isEditable() {
return false;
}

public async create(initialInput: CardEmbeddableInput, parent?: IContainer) {
return new CardEmbeddable(initialInput, parent);
}

public getDisplayName() {
return i18n.translate('contentManagement.embeddable.card', {
defaultMessage: 'Card',
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';

import {
IContainer,
withEmbeddableSubscription,
ContainerInput,
ContainerOutput,
EmbeddableStart,
} from '../../../../embeddable/public';

interface Props {
embeddable: IContainer;
input: ContainerInput;
embeddableServices: EmbeddableStart;
}

const CardListInner = ({ embeddable, input, embeddableServices }: Props) => {
const cards = Object.values(input.panels).map((panel) => {
const child = embeddable.getChild(panel.explicitInput.id);
return (
<EuiFlexItem key={panel.explicitInput.id}>
<embeddableServices.EmbeddablePanel embeddable={child} />
</EuiFlexItem>
);
});
return (
<EuiFlexGrid gutterSize="s" columns={4}>
{cards}
</EuiFlexGrid>
);
};

export const CardList = withEmbeddableSubscription<
ContainerInput,
ContainerOutput,
IContainer,
{ embeddableServices: EmbeddableStart }
>(CardListInner);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import ReactDOM from 'react-dom';

import { Embeddable, EmbeddableInput, IContainer } from '../../../embeddable/public';

export const CUSTOM_CONTENT_EMBEDDABLE = 'custom_content_embeddable';
export type CustomContentEmbeddableInput = EmbeddableInput & { render: () => React.ReactElement };

export class CustomContentEmbeddable extends Embeddable<CustomContentEmbeddableInput> {
public readonly type = CUSTOM_CONTENT_EMBEDDABLE;
private node: HTMLElement | null = null;

constructor(initialInput: CustomContentEmbeddableInput, parent?: IContainer) {
super(initialInput, {}, parent);
}

public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(this.input.render(), node);
}

public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}

public reload() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';

import { EmbeddableFactoryDefinition, IContainer } from '../../../embeddable/public';
import {
CUSTOM_CONTENT_EMBEDDABLE,
CustomContentEmbeddable,
CustomContentEmbeddableInput,
} from './custom_content_embeddable';

export class CustomContentEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
public readonly type = CUSTOM_CONTENT_EMBEDDABLE;

public async isEditable() {
return false;
}

public async create(initialInput: CustomContentEmbeddableInput, parent?: IContainer) {
return new CustomContentEmbeddable(initialInput, parent);
}

public getDisplayName() {
return i18n.translate('contentManagement.embeddable.customContent', {
defaultMessage: 'Content',
});
}
}
6 changes: 6 additions & 0 deletions src/plugins/content_management/public/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './page_render';
35 changes: 35 additions & 0 deletions src/plugins/content_management/public/components/page_render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { useObservable } from 'react-use';

import { Page } from '../services';
import { SectionRender } from './section_render';
import { EmbeddableStart } from '../../../embeddable/public';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';

Check failure on line 12 in src/plugins/content_management/public/components/page_render.tsx

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

`opensearch-dashboards/public` import should occur before import of `../services`

export interface Props {
page: Page;
embeddable: EmbeddableStart;
savedObjectsClient: SavedObjectsClientContract;
}

export const PageRender = ({ page, embeddable, savedObjectsClient }: Props) => {
const sections = useObservable(page.getSections$()) || [];

return (
<div className="contentManagement-page" style={{ margin: '10px 20px' }}>
{sections.map((section) => (
<SectionRender
embeddable={embeddable}
section={section}
savedObjectsClient={savedObjectsClient}
contents$={page.getContents$(section.id)}
/>
))}
</div>
);
};
Loading

0 comments on commit d73444b

Please sign in to comment.