Skip to content

Commit

Permalink
feat: register sample data as standalone app (opensearch-project#8)
Browse files Browse the repository at this point in the history
* feat: register sample data as standalone app

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: optimize code

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: add comment

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: use props to pass homeLink

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: add unit test

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

---------

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe authored May 6, 2024
1 parent b1da07e commit b4c0c27
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/plugins/home/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
export const PLUGIN_ID = 'home';
export const HOME_APP_BASE_PATH = `/app/${PLUGIN_ID}`;
export const USE_NEW_HOME_PAGE = 'home:useNewHomePage';
export const IMPORT_SAMPLE_DATA_APP_ID = 'import_sample_data';
45 changes: 45 additions & 0 deletions src/plugins/home/public/application/application.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect, useRef } from 'react';
import { render } from '@testing-library/react';
import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks';
import { renderImportSampleDataApp } from './application';

jest.mock('./components/home_app', () => ({
HomeApp: () => 'HomeApp',
ImportSampleDataApp: () => 'ImportSampleDataApp',
}));

const coreStartMocks = coreMock.createStart();

const ComponentForRender = (props: { renderFn: typeof renderImportSampleDataApp }) => {
const container = useRef<HTMLDivElement>(null);
const historyMock = scopedHistoryMock.create();
historyMock.listen.mockReturnValueOnce(() => () => null);
useEffect(() => {
if (container.current) {
const destroyFn = props.renderFn(container.current, coreStartMocks, historyMock);
return () => {
destroyFn.then((res) => res());
};
}
}, [historyMock, props]);

return <div ref={container} />;
};

describe('renderImportSampleDataApp', () => {
it('should render ImportSampleDataApp when calling renderImportSampleDataApp', async () => {
const { container } = render(<ComponentForRender renderFn={renderImportSampleDataApp} />);
expect(container).toMatchInlineSnapshot(`
<div>
<div>
ImportSampleDataApp
</div>
</div>
`);
});
});
27 changes: 26 additions & 1 deletion src/plugins/home/public/application/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { i18n } from '@osd/i18n';
import { ScopedHistory, CoreStart } from 'opensearch-dashboards/public';
import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public';
// @ts-ignore
import { HomeApp } from './components/home_app';
import { HomeApp, ImportSampleDataApp } from './components/home_app';
import { getServices } from './opensearch_dashboards_services';

import './index.scss';
Expand Down Expand Up @@ -77,3 +77,28 @@ export const renderApp = async (
unlisten();
};
};

export const renderImportSampleDataApp = async (
element: HTMLElement,
coreStart: CoreStart,
history: ScopedHistory
) => {
// dispatch synthetic hash change event to update hash history objects
// this is necessary because hash updates triggered by using popState won't trigger this event naturally.
// This must be called before the app is mounted to avoid call this after the redirect to default app logic kicks in
const unlisten = history.listen((location) => {
window.dispatchEvent(new HashChangeEvent('hashchange'));
});

render(
<OpenSearchDashboardsContextProvider services={{ ...coreStart }}>
<ImportSampleDataApp />
</OpenSearchDashboardsContextProvider>,
element
);

return () => {
unmountComponentAtNode(element);
unlisten();
};
};
48 changes: 38 additions & 10 deletions src/plugins/home/public/application/components/home_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,44 @@ const RedirectToDefaultApp = () => {
return null;
};

const renderTutorialDirectory = (props) => {
const { addBasePath, environmentService } = getServices();
const environment = environmentService.getEnvironment();
const isCloudEnabled = environment.cloud;

return (
<TutorialDirectory
addBasePath={addBasePath}
openTab={props.match.params.tab}
isCloudEnabled={isCloudEnabled}
withoutHomeBreadCrumb={props.withoutHomeBreadCrumb}
/>
);
};

export function ImportSampleDataApp() {
return (
<I18nProvider>
<Router>
<Switch>
<Route
path="*"
exact={true}
component={(props) =>
renderTutorialDirectory({
...props,
// For standalone import sample data application
// home breadcrumb should not be appended as it is not a sub app of home
withoutHomeBreadCrumb: true,
})
}
/>
</Switch>
</Router>
</I18nProvider>
);
}

export function HomeApp({ directories, solutions }) {
const {
savedObjectsClient,
Expand All @@ -63,16 +101,6 @@ export function HomeApp({ directories, solutions }) {
const environment = environmentService.getEnvironment();
const isCloudEnabled = environment.cloud;

const renderTutorialDirectory = (props) => {
return (
<TutorialDirectory
addBasePath={addBasePath}
openTab={props.match.params.tab}
isCloudEnabled={isCloudEnabled}
/>
);
};

const renderTutorial = (props) => {
return (
<Tutorial
Expand Down
60 changes: 60 additions & 0 deletions src/plugins/home/public/application/components/home_app.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render } from '@testing-library/react';
import { setServices } from '../opensearch_dashboards_services';
import { getMockedServices } from '../opensearch_dashboards_services.mock';
import { ImportSampleDataApp, HomeApp } from './home_app';

jest.mock('./legacy/home', () => ({
Home: () => <div>Home</div>,
}));

jest.mock('../load_tutorials', () => ({
getTutorial: () => {},
}));

jest.mock('./tutorial_directory', () => ({
TutorialDirectory: (props: { withoutHomeBreadCrumb?: boolean }) => (
<div
data-test-subj="tutorial_directory"
data-without-home-bread-crumb={!!props.withoutHomeBreadCrumb}
/>
),
}));

describe('<HomeApp />', () => {
let currentService: ReturnType<typeof getMockedServices>;
beforeEach(() => {
currentService = getMockedServices();
setServices(currentService);
});

it('should not pass withoutHomeBreadCrumb to TutorialDirectory component', async () => {
const originalHash = window.location.hash;
const { findByTestId } = render(<HomeApp />);
window.location.hash = '/tutorial_directory';
const tutorialRenderResult = await findByTestId('tutorial_directory');
expect(tutorialRenderResult.dataset.withoutHomeBreadCrumb).toEqual('false');

// revert to original hash
window.location.hash = originalHash;
});
});

describe('<ImportSampleDataApp />', () => {
let currentService: ReturnType<typeof getMockedServices>;
beforeEach(() => {
currentService = getMockedServices();
setServices(currentService);
});

it('should pass withoutHomeBreadCrumb to TutorialDirectory component', async () => {
const { findByTestId } = render(<ImportSampleDataApp />);
const tutorialRenderResult = await findByTestId('tutorial_directory');
expect(tutorialRenderResult.dataset.withoutHomeBreadCrumb).toEqual('true');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,17 @@ class TutorialDirectoryUi extends React.Component {

async componentDidMount() {
this._isMounted = true;

getServices().chrome.setBreadcrumbs([
{
const { chrome } = getServices();
const { withoutHomeBreadCrumb } = this.props;
const breadcrumbs = [{ text: addDataTitle }];
if (!withoutHomeBreadCrumb) {
breadcrumbs.splice(0, 0, {
text: homeTitle,
href: '#/',
},
{ text: addDataTitle },
]);
});
}

chrome.setBreadcrumbs(breadcrumbs);

const tutorialConfigs = await getTutorials();

Expand Down Expand Up @@ -322,6 +325,7 @@ TutorialDirectoryUi.propTypes = {
addBasePath: PropTypes.func.isRequired,
openTab: PropTypes.string,
isCloudEnabled: PropTypes.bool.isRequired,
withoutHomeBreadCrumb: PropTypes.bool,
};

export const TutorialDirectory = injectI18n(TutorialDirectoryUi);
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { coreMock } from '../../../../../core/public/mocks';
import { setServices } from '../opensearch_dashboards_services';
import { getMockedServices } from '../opensearch_dashboards_services.mock';

const makeProps = () => {
const coreMocks = coreMock.createStart();
return {
addBasePath: coreMocks.http.basePath.prepend,
openTab: 'foo',
isCloudEnabled: false,
};
};

describe('<TutorialDirectory />', () => {
let currentService: ReturnType<typeof getMockedServices>;
beforeEach(() => {
currentService = getMockedServices();
setServices(currentService);
});
it('should render home breadcrumbs when withoutHomeBreadCrumb is undefined', async () => {
const finalProps = makeProps();
currentService.http.get.mockResolvedValueOnce([]);
// @ts-ignore
const { TutorialDirectory } = await import('./tutorial_directory');
render(
<IntlProvider locale="en">
<TutorialDirectory {...finalProps} />
</IntlProvider>
);
expect(currentService.chrome.setBreadcrumbs).toBeCalledWith([
{
href: '#/',
text: 'Home',
},
{
text: 'Add data',
},
]);
});

it('should not render home breadcrumbs when withoutHomeBreadCrumb is true', async () => {
const finalProps = makeProps();
currentService.http.get.mockResolvedValueOnce([]);
// @ts-ignore
const { TutorialDirectory } = await import('./tutorial_directory');
render(
<IntlProvider locale="en">
<TutorialDirectory {...finalProps} withoutHomeBreadCrumb />
</IntlProvider>
);
expect(currentService.chrome.setBreadcrumbs).toBeCalledWith([
{
text: 'Add data',
},
]);
});
});
2 changes: 1 addition & 1 deletion src/plugins/home/public/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
* under the License.
*/

export { renderApp } from './application';
export { renderApp, renderImportSampleDataApp } from './application';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { coreMock } from '../../../../core/public/mocks';
import { urlForwardingPluginMock } from '../../../url_forwarding/public/mocks';
import { homePluginMock } from '../mocks';
import {
EnvironmentService,
FeatureCatalogueRegistry,
SectionTypeService,
TutorialService,
} from '../services';
import { telemetryPluginMock } from '../../../telemetry/public/mocks';

export const getMockedServices = () => {
const coreMocks = coreMock.createStart();
const urlForwarding = urlForwardingPluginMock.createStartContract();
const homePlugin = homePluginMock.createSetupContract();
return {
...coreMocks,
...homePlugin,
telemetry: telemetryPluginMock.createStartContract(),
indexPatternService: jest.fn(),
dataSource: {
dataSourceEnabled: false,
hideLocalCluster: false,
noAuthenticationTypeEnabled: false,
usernamePasswordAuthEnabled: false,
awsSigV4AuthEnabled: false,
},
opensearchDashboardsVersion: '',
urlForwarding,
savedObjectsClient: coreMocks.savedObjects.client,
toastNotifications: coreMocks.notifications.toasts,
banners: coreMocks.overlays.banners,
trackUiMetric: jest.fn(),
getBasePath: jest.fn(),
addBasePath: jest.fn(),
environmentService: new EnvironmentService(),
tutorialService: new TutorialService(),
homeConfig: homePlugin.config,
featureCatalogue: new FeatureCatalogueRegistry(),
sectionTypes: new SectionTypeService(),
};
};
8 changes: 8 additions & 0 deletions src/plugins/home/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,13 @@ describe('HomePublicPlugin', () => {
expect(setup).toHaveProperty('tutorials');
expect(setup.tutorials).toHaveProperty('setVariable');
});

test('wires up and register applications', async () => {
const coreMocks = coreMock.createSetup();
await new HomePublicPlugin(mockInitializerContext).setup(coreMocks, {
urlForwarding: urlForwardingPluginMock.createSetupContract(),
});
expect(coreMocks.application.register).toBeCalledTimes(2);
});
});
});
Loading

0 comments on commit b4c0c27

Please sign in to comment.