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 (
+
+ );
+ }
+}
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 a6618d2ecdfb..e7503cba645a 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
@@ -39,6 +39,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(),
+ },
};
}