From ae437b9d3831d675191aa4e2d4b820805d61a2d1 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Thu, 7 Mar 2024 17:41:18 -0800 Subject: [PATCH] [MD] Create data source menu component able to be mount to nav bar (#6082) * create data source menu component Signed-off-by: Lu Yu * add changelog Signed-off-by: Lu Yu --------- Signed-off-by: Lu Yu --- CHANGELOG.md | 2 +- .../create_data_source_menu.test.tsx.snap | 146 +++++++++++++++ .../data_source_menu.test.tsx.snap | 67 +++++++ .../data_source_selectable.test.tsx.snap | 123 +++++++++++++ .../create_data_source_menu.test.tsx | 43 +++++ .../create_data_source_menu.tsx | 13 ++ .../data_source_menu.test.tsx | 55 ++++++ .../data_source_menu/data_source_menu.tsx | 94 ++++++++++ .../data_source_selectable.test.tsx | 63 +++++++ .../data_source_selectable.tsx | 172 ++++++++++++++++++ .../components/data_source_menu/index.ts | 6 + .../data_source_selector.tsx | 1 + .../data_source_management/public/index.ts | 1 + .../data_source_management/public/plugin.ts | 12 +- 14 files changed, 795 insertions(+), 3 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_menu.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_selectable.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.test.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.test.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.tsx create mode 100644 src/plugins/data_source_management/public/components/data_source_menu/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f44d1bca7f7a..5dbfcbd705fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Optional workspaces params in repository ([#5949](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5949)) - [Multiple Datasource] Refactoring create and edit form to use authentication registry ([#6002](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6002)) - [Multiple Datasource] Expose a few properties for customize the appearance of the data source selector component ([#6057](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6057)) - +- [Multiple Datasource] Create data source menu component able to be mount to nav bar ([#6082](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6082)) ### 🐛 Bug Fixes diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap new file mode 100644 index 000000000000..207e38d657da --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create data source menu should render normally 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+ +
+ , + "container":
+ +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_menu.test.tsx.snap new file mode 100644 index 000000000000..19f1e1c660d2 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_menu.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataSourceMenu should render normally with local cluster is hidden 1`] = ` + + + + + +`; + +exports[`DataSourceMenu should render normally with local cluster not hidden 1`] = ` + + + + + +`; diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_selectable.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_selectable.test.tsx.snap new file mode 100644 index 000000000000..ff494ad932e3 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/data_source_selectable.test.tsx.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataSourceSelectable should render normally with local cluster is hidden 1`] = ` + + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSelectableContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + + + + + + + + + +`; + +exports[`DataSourceSelectable should render normally with local cluster not hidden 1`] = ` + + + + Local cluster + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSelectableContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + + + + + + + + + +`; diff --git a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx new file mode 100644 index 000000000000..1ab059c6954c --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createDataSourceMenu } from './create_data_source_menu'; +import { SavedObjectsClientContract } from '../../../../../core/public'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; +import React from 'react'; +import { render } from '@testing-library/react'; + +describe('create data source menu', () => { + let client: SavedObjectsClientContract; + const notifications = notificationServiceMock.createStartContract(); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + }); + + it('should render normally', () => { + const props = { + showDataSourceSelectable: true, + appName: 'myapp', + savedObjects: client, + notifications, + fullWidth: true, + hideLocalCluster: true, + disableDataSourceSelectable: false, + className: 'myclass', + }; + const TestComponent = createDataSourceMenu(); + const component = render(); + expect(component).toMatchSnapshot(); + expect(client.find).toBeCalledWith({ + fields: ['id', 'description', 'title'], + perPage: 10000, + type: 'data-source', + }); + expect(notifications.toasts.addWarning).toBeCalledTimes(0); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx new file mode 100644 index 000000000000..7d5972f8e068 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DataSourceMenu, DataSourceMenuProps } from './data_source_menu'; + +export function createDataSourceMenu() { + return (props: DataSourceMenuProps) => { + return ; + }; +} diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.test.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.test.tsx new file mode 100644 index 000000000000..2653346ec879 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ShallowWrapper, shallow } from 'enzyme'; +import { SavedObjectsClientContract } from '../../../../../core/public'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; +import React from 'react'; +import { DataSourceMenu } from './data_source_menu'; + +describe('DataSourceMenu', () => { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + + let client: SavedObjectsClientContract; + const notifications = notificationServiceMock.createStartContract(); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + }); + + it('should render normally with local cluster not hidden', () => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render normally with local cluster is hidden', () => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx new file mode 100644 index 000000000000..57e3590f5bc1 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactElement } from 'react'; +import { EuiHeaderLinks } from '@elastic/eui'; +import classNames from 'classnames'; + +import { + MountPoint, + NotificationsStart, + SavedObjectsClientContract, +} from '../../../../../core/public'; +import { MountPointPortal } from '../../../../opensearch_dashboards_react/public'; +import { DataSourceSelectable } from './data_source_selectable'; +import { DataSourceOption } from '../data_source_selector/data_source_selector'; + +export interface DataSourceMenuProps { + showDataSourceSelectable: boolean; + appName: string; + savedObjects: SavedObjectsClientContract; + notifications: NotificationsStart; + fullWidth: boolean; + hideLocalCluster: boolean; + dataSourceCallBackFunc: (dataSource: DataSourceOption) => void; + disableDataSourceSelectable?: boolean; + className?: string; + selectedOption?: DataSourceOption[]; + setMenuMountPoint?: (menuMount: MountPoint | undefined) => void; +} + +export function DataSourceMenu(props: DataSourceMenuProps): ReactElement | null { + const { + savedObjects, + notifications, + dataSourceCallBackFunc, + showDataSourceSelectable, + disableDataSourceSelectable, + fullWidth, + hideLocalCluster, + selectedOption, + } = props; + + if (!showDataSourceSelectable) { + return null; + } + + function renderMenu(className: string): ReactElement | null { + if (!showDataSourceSelectable) return null; + return ( + + {renderDataSourceSelectable()} + + ); + } + + function renderDataSourceSelectable(): ReactElement | null { + if (!showDataSourceSelectable) return null; + return ( + 0 ? selectedOption : undefined} + /> + ); + } + + function renderLayout() { + const { setMenuMountPoint } = props; + const menuClassName = classNames('osdTopNavMenu', props.className); + if (setMenuMountPoint) { + return ( + <> + + {renderMenu(menuClassName)} + + + ); + } else { + return <>{renderMenu(menuClassName)}; + } + } + + return renderLayout(); +} + +DataSourceMenu.defaultProps = { + disableDataSourceSelectable: false, +}; diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.test.tsx new file mode 100644 index 000000000000..192aea1d642c --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ShallowWrapper, shallow } from 'enzyme'; +import { SavedObjectsClientContract } from '../../../../../core/public'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; +import React from 'react'; +import { DataSourceSelectable } from './data_source_selectable'; + +describe('DataSourceSelectable', () => { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + }); + + it('should render normally with local cluster not hidden', () => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + expect(client.find).toBeCalledWith({ + fields: ['id', 'description', 'title'], + perPage: 10000, + type: 'data-source', + }); + expect(toasts.addWarning).toBeCalledTimes(0); + }); + + it('should render normally with local cluster is hidden', () => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + expect(client.find).toBeCalledWith({ + fields: ['id', 'description', 'title'], + perPage: 10000, + type: 'data-source', + }); + expect(toasts.addWarning).toBeCalledTimes(0); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.tsx new file mode 100644 index 000000000000..1c8c6bd29210 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_selectable.tsx @@ -0,0 +1,172 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiIcon, + EuiPopover, + EuiContextMenuPanel, + EuiPanel, + EuiButtonEmpty, + EuiSelectable, + EuiSpacer, +} from '@elastic/eui'; +import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; +import { getDataSources } from '../utils'; +import { DataSourceOption, LocalCluster } from '../data_source_selector/data_source_selector'; + +interface DataSourceSelectableProps { + savedObjectsClient: SavedObjectsClientContract; + notifications: ToastsStart; + onSelectedDataSource: (dataSource: DataSourceOption) => void; + disabled: boolean; + hideLocalCluster: boolean; + fullWidth: boolean; + selectedOption?: DataSourceOption[]; +} + +interface DataSourceSelectableState { + dataSourceOptions: DataSourceOption[]; + selectedOption: DataSourceOption[]; + isPopoverOpen: boolean; +} + +export class DataSourceSelectable extends React.Component< + DataSourceSelectableProps, + DataSourceSelectableState +> { + private _isMounted: boolean = false; + + constructor(props: DataSourceSelectableProps) { + super(props); + + this.state = { + isPopoverOpen: false, + selectedOption: this.props.selectedOption + ? this.props.selectedOption + : this.props.hideLocalCluster + ? [] + : [LocalCluster], + }; + + this.onChange.bind(this); + } + + componentWillUnmount() { + this._isMounted = false; + } + + onClick() { + this.setState({ ...this.state, isPopoverOpen: !this.state.isPopoverOpen }); + } + + closePopover() { + this.setState({ ...this.state, isPopoverOpen: false }); + } + + async componentDidMount() { + this._isMounted = true; + getDataSources(this.props.savedObjectsClient) + .then((fetchedDataSources) => { + if (fetchedDataSources?.length) { + let dataSourceOptions = fetchedDataSources.map((dataSource) => ({ + id: dataSource.id, + label: dataSource.title, + })); + + dataSourceOptions = dataSourceOptions.sort((a, b) => + a.label.toLowerCase().localeCompare(b.label.toLowerCase()) + ); + + if (!this.props.hideLocalCluster) { + dataSourceOptions.unshift(LocalCluster); + } + + if (!this._isMounted) return; + this.setState({ + ...this.state, + dataSourceOptions, + }); + } + }) + .catch(() => { + this.props.notifications.addWarning( + i18n.translate('dataSource.fetchDataSourceError', { + defaultMessage: 'Unable to fetch existing data sources', + }) + ); + }); + } + + onChange(options) { + if (!this._isMounted) return; + const selectedDataSource = options.find(({ checked }) => checked); + + this.setState({ + selectedOption: [selectedDataSource], + }); + this.props.onSelectedDataSource({ ...selectedDataSource }); + } + + render() { + const button = ( + <> + + + {(this.state.selectedOption && + this.state.selectedOption.length > 0 && + this.state.selectedOption[0].label) || + ''} + + + ); + + return ( + + + + + this.onChange(newOptions)} + singleSelection={true} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + + + ); + } +} diff --git a/src/plugins/data_source_management/public/components/data_source_menu/index.ts b/src/plugins/data_source_management/public/components/data_source_menu/index.ts new file mode 100644 index 000000000000..21951dc8d29e --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_menu/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { DataSourceMenu } from './data_source_menu'; diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx index c6551f92ae50..d2f92e89388a 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx @@ -38,6 +38,7 @@ interface DataSourceSelectorState { export interface DataSourceOption { label: string; id: string; + checked?: string; } export class DataSourceSelector extends React.Component< diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts index 0b12763b5a43..5e2e9b647396 100644 --- a/src/plugins/data_source_management/public/index.ts +++ b/src/plugins/data_source_management/public/index.ts @@ -12,4 +12,5 @@ export function plugin() { } export { DataSourceManagementPluginStart } from './types'; export { DataSourceSelector } from './components/data_source_selector'; +export { DataSourceMenu } from './components/data_source_menu'; export { DataSourceManagementPlugin, DataSourceManagementPluginSetup } from './plugin'; diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index d5b2117e800b..12cab715b205 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -19,6 +19,8 @@ import { } from './auth_registry'; import { noAuthCredentialAuthMethod, sigV4AuthMethod, usernamePasswordAuthMethod } from './types'; import { DataSourceSelectorProps } from './components/data_source_selector/data_source_selector'; +import { createDataSourceMenu } from './components/data_source_menu/create_data_source_menu'; +import { DataSourceMenuProps } from './components/data_source_menu/data_source_menu'; export interface DataSourceManagementSetupDependencies { management: ManagementSetup; @@ -28,7 +30,10 @@ export interface DataSourceManagementSetupDependencies { export interface DataSourceManagementPluginSetup { registerAuthenticationMethod: (authMethodValues: AuthenticationMethod) => void; - getDataSourceSelector: React.ComponentType; + ui: { + DataSourceSelector: React.ComponentType; + DataSourceMenu: React.ComponentType; + }; } export interface DataSourceManagementPluginStart { @@ -96,7 +101,10 @@ export class DataSourceManagementPlugin return { registerAuthenticationMethod, - getDataSourceSelector: createDataSourceSelector(), + ui: { + DataSourceSelector: createDataSourceSelector(), + DataSourceMenu: createDataSourceMenu(), + }, }; }