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 <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe committed Jul 17, 2024
1 parent dd6c922 commit eaf5fea
Show file tree
Hide file tree
Showing 39 changed files with 2,025 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} />;

Check warning on line 37 in src/plugins/content_management/public/app.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/app.tsx#L37

Added line #L37 was not covered by tests
};

export const renderApp = (
{ params, pages, coreStart, depsStart }: Props,
element: AppMountParameters['element']
) => {
ReactDOM.render(

Check warning on line 44 in src/plugins/content_management/public/app.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/app.tsx#L44

Added line #L44 was not covered by tests
<I18nProvider>
<Router history={params.history}>
<Switch>
{pages.map((page) => (
<Route path={[`/${page.config.id}`]}>

Check warning on line 49 in src/plugins/content_management/public/app.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/app.tsx#L49

Added line #L49 was not covered by tests
{renderPage({
page,
embeddable: depsStart.embeddable,
savedObjectsClient: coreStart.savedObjects.client,
})}
</Route>
))}
</Switch>
</Router>
</I18nProvider>,
element
);

return () => ReactDOM.unmountComponentAtNode(element);

Check warning on line 63 in src/plugins/content_management/public/app.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/app.tsx#L63

Added line #L63 was not covered by tests
};
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 { embeddablePluginMock } from '../../../../embeddable/public/mocks';
import { CardContainer } from './card_container';

jest.mock('./card_list', () => {
return {
CardList: jest.fn().mockReturnValue(<span id="mockCardList"></span>),

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

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Empty components are self-closing
};
});

test('CardContainer should render CardList', () => {
const container = new CardContainer(
{ id: 'container-id', panels: {} },
embeddablePluginMock.createStartContract()
);
const node = document.createElement('div');
container.render(node);
expect(node.querySelector('#mockCardList')).toBeTruthy();

container.destroy();
expect(node.querySelector('#mockCardList')).toBeFalsy();
});
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<{}, CardContainerInput> {
public readonly type = CARD_CONTAINER;
private node?: HTMLElement;

constructor(input: CardContainerInput, 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);

Check warning on line 31 in src/plugins/content_management/public/components/card_container/card_container.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/components/card_container/card_container.tsx#L31

Added line #L31 was not covered by tests
}
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,20 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { embeddablePluginMock } from '../../../../embeddable/public/mocks';
import { CARD_CONTAINER, CardContainer } from './card_container';
import { CardContainerFactoryDefinition } from './card_container_factory';

test('CardContainerFactoryDefinition', async () => {
const getStartServices = jest
.fn()
.mockResolvedValue({ embeddableServices: embeddablePluginMock.createStartContract() });
const factory = new CardContainerFactoryDefinition(getStartServices);
expect(factory.type).toBe(CARD_CONTAINER);
expect(factory.isContainerType).toBe(true);
expect(await factory.isEditable()).toBe(false);
expect(factory.getDisplayName()).toBe('Card container');
expect(await factory.create({ id: 'card-id', panels: {} })).toBeInstanceOf(CardContainer);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

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

interface StartServices {
embeddableServices: EmbeddableStart;
}

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

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

public async isEditable() {
return false;
}

public create = async (initialInput: CardContainerInput) => {
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,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CardEmbeddable } from './card_embeddable';

test('CardEmbeddable should render a card with the title', () => {
const embeddable = new CardEmbeddable({ id: 'card-id', title: 'card title', description: '' });

const node = document.createElement('div');
embeddable.render(node);

// it should render the card with title specified
expect(
Array.from(node.querySelectorAll('*')).find((ele) => ele.textContent?.trim() === 'card title')
).toBeTruthy();

embeddable.destroy();
expect(
Array.from(node.querySelectorAll('*')).find((ele) => ele.textContent?.trim() === 'card title')
).toBeFalsy();
});
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);

Check warning on line 24 in src/plugins/content_management/public/components/card_container/card_embeddable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/components/card_container/card_embeddable.tsx#L24

Added line #L24 was not covered by tests
}
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,17 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CARD_EMBEDDABLE, CardEmbeddable } from './card_embeddable';
import { CardEmbeddableFactoryDefinition } from './card_embeddable_factory';

test('create CardEmbeddableFactoryDefinition', async () => {
const factory = new CardEmbeddableFactoryDefinition();
expect(factory.type).toBe(CARD_EMBEDDABLE);
expect(factory.getDisplayName()).toBe('Card');
expect(await factory.isEditable()).toBe(false);
expect(await factory.create({ id: 'card-id', title: 'title', description: '' })).toBeInstanceOf(
CardEmbeddable
);
});
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,41 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen } from '@testing-library/react';

import { CardList } from './card_list';
import { CardContainer } from './card_container';
import { embeddablePluginMock } from '../../../../embeddable/public/mocks';
import { CARD_EMBEDDABLE } from './card_embeddable';

beforeEach(() => {
jest.restoreAllMocks();
});

test('render list of cards', () => {
const embeddableStart = embeddablePluginMock.createStartContract();
jest
.spyOn(embeddableStart, 'EmbeddablePanel')
.mockImplementation(() => <span>CardEmbeddablePanel</span>);
render(
<CardList
embeddableServices={embeddableStart}
embeddable={
new CardContainer(
{
id: 'card',
panels: {
'card-id-1': { type: CARD_EMBEDDABLE, explicitInput: { id: 'card-id-1' } },
'card-id-2': { type: CARD_EMBEDDABLE, explicitInput: { id: 'card-id-2' } },
},
},
embeddableStart
)
}
/>
);
expect(screen.queryAllByText('CardEmbeddablePanel')).toHaveLength(2);
});
Loading

0 comments on commit eaf5fea

Please sign in to comment.