diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1f58d242853..f48ab192f428 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Workspace] Register a workspace dropdown menu at the top of left nav bar ([#6150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6150))
- [Multiple Datasource] Add icon in datasource table page to show the default datasource ([#6231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6231))
- [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171))
+- [Multiple Datasource] Add multi selectable data source component ([#6211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6211))
- [Multiple Datasource] Refactor data source menu and interface to allow cleaner selection of component and related configurations ([#6256](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6256))
- [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179))
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
index 6fa9b51ce3e7..9db092a1b61c 100644
--- 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
@@ -161,6 +161,67 @@ Object {
}
`;
+exports[`DataSourceMenu should render data source multi select component 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],
+}
+`;
+
exports[`DataSourceMenu should render data source selectable only with local cluster is hidden 1`] = `
`;
+
+exports[`DataSourceSelectable should show popover when click on button 1`] = `
+Object {
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+
+
+
+
+ You are in a dialog. To close this dialog, hit escape.
+
+
+
+
+
+
+
+ ,
+ "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/data_source_menu.test.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.test.tsx
index af5ef0cf3f32..4f7914148ca8 100644
--- 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
@@ -94,4 +94,18 @@ describe('DataSourceMenu', () => {
);
expect(container).toMatchSnapshot();
});
+
+ it('should render data source multi select component', () => {
+ const container = render(
+
+ );
+ expect(container).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
index c5d4cf421696..4015e246853f 100644
--- 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
@@ -8,10 +8,12 @@ import React, { ReactElement } from 'react';
import { DataSourceSelectable } from './data_source_selectable';
import { DataSourceAggregatedView } from '../data_source_aggregated_view';
import { DataSourceView } from '../data_source_view';
+import { DataSourceMultiSelectable } from '../data_source_multi_selectable/data_source_multi_selectable';
import {
DataSourceAggregatedViewConfig,
DataSourceComponentType,
DataSourceMenuProps,
+ DataSourceMultiSelectableConfig,
DataSourceSelectableConfig,
DataSourceViewConfig,
} from './types';
@@ -29,6 +31,27 @@ export function DataSourceMenu(props: DataSourceMenuProps): ReactElement |
);
}
+ function renderDataSourceMultiSelectable(
+ config: DataSourceMultiSelectableConfig
+ ): ReactElement | null {
+ const {
+ fullWidth,
+ hideLocalCluster,
+ savedObjects,
+ notifications,
+ onSelectedDataSources,
+ } = config;
+ return (
+
+ );
+ }
+
function renderDataSourceSelectable(config: DataSourceSelectableConfig): ReactElement | null {
const {
onSelectedDataSources,
@@ -87,6 +110,8 @@ export function DataSourceMenu(props: DataSourceMenuProps): ReactElement |
return renderDataSourceSelectable(componentConfig as DataSourceSelectableConfig);
case DataSourceComponentType.DataSourceView:
return renderDataSourceView(componentConfig as DataSourceViewConfig);
+ case DataSourceComponentType.DataSourceMultiSelectable:
+ return renderDataSourceMultiSelectable(componentConfig as DataSourceMultiSelectableConfig);
default:
return null;
}
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
index b9f06ce3f8bb..74c6d5d9e5a6 100644
--- 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
@@ -10,6 +10,7 @@ import React from 'react';
import { DataSourceSelectable } from './data_source_selectable';
import { AuthType } from '../../types';
import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks';
+import { render } from '@testing-library/react';
describe('DataSourceSelectable', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
@@ -82,4 +83,22 @@ describe('DataSourceSelectable', () => {
expect(component).toMatchSnapshot();
expect(toasts.addWarning).toBeCalledTimes(0);
});
+
+ it('should show popover when click on button', async () => {
+ const onSelectedDataSource = jest.fn();
+ const container = render(
+ ds.attributes.auth.type !== AuthType.NoAuth}
+ />
+ );
+ const button = await container.findByTestId('dataSourceSelectableContextMenuHeaderLink');
+ button.click();
+ expect(container).toMatchSnapshot();
+ });
});
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
index f8b19271f13b..b2c8cdbe9db0 100644
--- 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
@@ -120,6 +120,7 @@ export class DataSourceSelectable extends React.Component<
this.setState({
selectedOption: [selectedDataSource],
});
+
this.props.onSelectedDataSources([selectedDataSource]);
}
@@ -167,6 +168,7 @@ export class DataSourceSelectable extends React.Component<
options={this.state.dataSourceOptions}
onChange={(newOptions) => this.onChange(newOptions)}
singleSelection={true}
+ data-test-subj={'dataSourceSelectable'}
>
{(list, search) => (
<>
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
index 2a764d602b56..216cf33c6ee3 100644
--- 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
@@ -10,4 +10,5 @@ export {
DataSourceComponentType,
DataSourceViewConfig,
DataSourceMenuProps,
+ DataSourceMultiSelectableConfig,
} from './types';
diff --git a/src/plugins/data_source_management/public/components/data_source_menu/types.ts b/src/plugins/data_source_management/public/components/data_source_menu/types.ts
index e570eb83ec82..4121edc0c863 100644
--- a/src/plugins/data_source_management/public/components/data_source_menu/types.ts
+++ b/src/plugins/data_source_management/public/components/data_source_menu/types.ts
@@ -30,6 +30,7 @@ export const DataSourceComponentType = {
DataSourceSelectable: 'DataSourceSelectable',
DataSourceView: 'DataSourceView',
DataSourceAggregatedView: 'DataSourceAggregatedView',
+ DataSourceMultiSelectable: 'DataSourceMultiSelectable',
} as const;
export type DataSourceComponentType = typeof DataSourceComponentType[keyof typeof DataSourceComponentType];
@@ -57,3 +58,10 @@ export interface DataSourceSelectableConfig extends DataSourceBaseConfig {
hideLocalCluster?: boolean;
dataSourceFilter?: (dataSource: SavedObject) => boolean;
}
+
+export interface DataSourceMultiSelectableConfig extends DataSourceBaseConfig {
+ onSelectedDataSources: (dataSources: DataSourceOption[]) => void;
+ savedObjects: SavedObjectsClientContract;
+ notifications: NotificationsStart;
+ hideLocalCluster?: boolean;
+}
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/__snapshots__/data_source_filter_group.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_multi_selectable/__snapshots__/data_source_filter_group.test.tsx.snap
new file mode 100644
index 000000000000..665f9cfa27dc
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/__snapshots__/data_source_filter_group.test.tsx.snap
@@ -0,0 +1,664 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DataSourceFilterGroup should render normally 1`] = `
+
+
+ Data sources
+
+
+ 1
+
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="popoverExampleMultiSelect"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="none"
+>
+
+
+
+
+
+ name1
+
+
+
+
+
+
+`;
+
+exports[`DataSourceFilterGroup should render popup when clicking on button 1`] = `
+Object {
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+
+
+ You are in a dialog. To close this dialog, hit escape.
+
+
+
+
+
+
+
+
+
+
+ name1
+
+
+
+
+
+
+
+
+
+ ,
+ "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],
+}
+`;
+
+exports[`DataSourceFilterGroup should toggle all when clicking on button and should search 1`] = `
+Object {
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+
+
+ You are in a dialog. To close this dialog, hit escape.
+
+
+
+
+
+
+
+
+
+
+ name1
+
+
+
+
+
+
+
+
+
+ ,
+ "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_multi_selectable/__snapshots__/data_source_multi_selectable.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_multi_selectable/__snapshots__/data_source_multi_selectable.test.tsx.snap
new file mode 100644
index 000000000000..627a1169c15b
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/__snapshots__/data_source_multi_selectable.test.tsx.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DataSourceMultiSelectable should render normally with local cluster hidden 1`] = `
+
+`;
+
+exports[`DataSourceMultiSelectable should render normally with local cluster not hidden 1`] = `
+
+`;
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.test.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.test.tsx
new file mode 100644
index 000000000000..62bf10ec8310
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.test.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ShallowWrapper, shallow } from 'enzyme';
+import React from 'react';
+import { DataSourceFilterGroup } from './data_source_filter_group';
+import { render, fireEvent, screen } from '@testing-library/react';
+
+describe('DataSourceFilterGroup', () => {
+ let component: ShallowWrapper, React.Component<{}, {}, any>>;
+
+ it('should render normally', () => {
+ const mockCallBack = jest.fn();
+ component = shallow(
+ mockCallBack(items)}
+ />
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should render popup when clicking on button', async () => {
+ const mockCallBack = jest.fn();
+ const container = render(
+ mockCallBack(items)}
+ />
+ );
+ const button = await container.findByTestId('dataSourceFilterGroupButton');
+ button.click();
+ expect(container).toMatchSnapshot();
+ expect(mockCallBack).toBeCalledTimes(0);
+
+ fireEvent.click(screen.getByText('name1'));
+ expect(mockCallBack).toBeCalledWith([
+ { checked: undefined, id: '1', label: 'name1', visible: true },
+ ]);
+ });
+
+ it('should toggle all when clicking on button and should search', async () => {
+ const mockCallBack = jest.fn();
+ const container = render(
+ mockCallBack(items)}
+ />
+ );
+ const button = await container.findByTestId('dataSourceFilterGroupButton');
+ button.click();
+
+ fireEvent.click(screen.getByText('Deselect all'));
+ expect(mockCallBack).toBeCalledWith([
+ { checked: undefined, id: '1', label: 'name1', visible: true },
+ ]);
+
+ fireEvent.click(screen.getByText('Select all'));
+ expect(mockCallBack).toBeCalledWith([
+ { checked: 'on', id: '1', label: 'name1', visible: true },
+ ]);
+
+ const input = screen.getByTestId('dataSourceMultiSelectFieldSearch');
+ fireEvent.change(input, { target: { value: 'random input' } });
+ fireEvent.keyDown(input, { key: 'enter', keyCode: 13 });
+
+ expect(mockCallBack).toBeCalledWith([
+ { checked: 'on', id: '1', label: 'name1', visible: false },
+ ]);
+
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.tsx
new file mode 100644
index 000000000000..8d0e22beadaf
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_filter_group.tsx
@@ -0,0 +1,166 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiNotificationBadge,
+ EuiFilterSelectItem,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiFieldSearch,
+ FilterChecked,
+ EuiPopoverFooter,
+ EuiButtonGroup,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { DataSourceOption } from '../data_source_selector/data_source_selector';
+
+export interface SelectedDataSourceOption extends DataSourceOption {
+ label: string;
+ id: string;
+ visible: boolean;
+ checked?: FilterChecked;
+}
+
+export interface DataSourceFilterGroupProps {
+ selectedOptions: SelectedDataSourceOption[];
+ setSelectedOptions: (options: SelectedDataSourceOption[]) => void;
+}
+
+type SelectionToggleOptionIds = 'select_all' | 'deselect_all';
+
+const selectionToggleButtons = [
+ {
+ id: 'select_all',
+ label: 'Select all',
+ },
+ {
+ id: 'deselect_all',
+ label: 'Deselect all',
+ },
+];
+
+export const DataSourceFilterGroup: React.FC = ({
+ selectedOptions,
+ setSelectedOptions,
+}) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const [selectionToggleSelectedId, setSelectionToggleSelectedId] = useState<
+ SelectionToggleOptionIds
+ >('select_all');
+
+ const onButtonClick = () => {
+ setIsPopoverOpen(!isPopoverOpen);
+ };
+
+ const closePopover = () => {
+ setIsPopoverOpen(false);
+ };
+
+ function toggleItem(index: number) {
+ if (!selectedOptions[index]) {
+ return;
+ }
+
+ const newItems = [...selectedOptions];
+
+ if (newItems[index].checked === 'on') {
+ newItems[index] = {
+ ...newItems[index],
+ checked: undefined,
+ };
+ } else {
+ newItems[index] = {
+ ...newItems[index],
+ checked: 'on',
+ };
+ }
+
+ setSelectedOptions(newItems);
+ }
+
+ function onSelectionToggleChange(optionId: string) {
+ setSelectionToggleSelectedId(optionId as SelectionToggleOptionIds);
+ toggleAll(optionId === 'select_all' ? 'on' : undefined);
+ }
+
+ function toggleAll(state: 'on' | undefined) {
+ const optionsAfterToggle = selectedOptions.map((option) => ({
+ ...option,
+ checked: state,
+ }));
+
+ setSelectedOptions(optionsAfterToggle);
+ }
+
+ function search(term: string) {
+ const optionsAfterSearch = selectedOptions.map((option) => {
+ option.visible = option.label.toLowerCase().includes(term.toLowerCase());
+ return option;
+ });
+ setSelectedOptions(optionsAfterSearch);
+ }
+
+ const numActiveSelections = selectedOptions.filter((option) => option.checked === 'on').length;
+ const button = (
+ <>
+
+ {'Data sources'}
+
+ {numActiveSelections}
+ >
+ );
+
+ return (
+
+
+
+
+
+ {selectedOptions.map((item, index) => {
+ const itemStyle: any = {};
+ itemStyle.display = !item.visible ? 'none' : itemStyle.display;
+
+ return (
+ toggleItem(index)}
+ showIcons={true}
+ style={itemStyle}
+ >
+ {item.label}
+
+ );
+ })}
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx
new file mode 100644
index 000000000000..afe65f554626
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
+import { notificationServiceMock } from '../../../../../core/public/mocks';
+import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks';
+import { ShallowWrapper, shallow } from 'enzyme';
+import { DataSourceMultiSelectable } from './data_source_multi_selectable';
+import React from 'react';
+import { render, fireEvent, screen } from '@testing-library/react';
+
+describe('DataSourceMultiSelectable', () => {
+ let component: ShallowWrapper, React.Component<{}, {}, any>>;
+
+ let client: SavedObjectsClientContract;
+ const { toasts } = notificationServiceMock.createStartContract();
+ const nextTick = () => new Promise((res) => process.nextTick(res));
+
+ beforeEach(() => {
+ client = {
+ find: jest.fn().mockResolvedValue([]),
+ } as any;
+ mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
+ });
+
+ it('should render normally with local cluster not hidden', () => {
+ component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ expect(client.find).toBeCalledWith({
+ fields: ['id', 'title', 'auth.type'],
+ perPage: 10000,
+ type: 'data-source',
+ });
+ expect(toasts.addWarning).toBeCalledTimes(0);
+ });
+
+ it('should render normally with local cluster hidden', () => {
+ component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ expect(client.find).toBeCalledWith({
+ fields: ['id', 'title', 'auth.type'],
+ perPage: 10000,
+ type: 'data-source',
+ });
+ expect(toasts.addWarning).toBeCalledTimes(0);
+ });
+
+ it('should show toasts when exception happens', async () => {
+ const errorClient = {
+ find: () => {
+ return new Promise((resolve, reject) => {
+ reject('error');
+ });
+ },
+ } as any;
+
+ component = shallow(
+
+ );
+ await nextTick();
+ expect(toasts.addWarning).toBeCalledTimes(1);
+ });
+
+ it('should callback when onChange happens', async () => {
+ const callbackMock = jest.fn();
+ const container = render(
+
+ );
+ const button = await container.findByTestId('dataSourceFilterGroupButton');
+ button.click();
+ fireEvent.click(screen.getByText('Deselect all'));
+
+ expect(callbackMock).toBeCalledWith([]);
+ });
+});
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx
new file mode 100644
index 000000000000..8405a37a43c2
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public';
+import { i18n } from '@osd/i18n';
+import { DataSourceFilterGroup, SelectedDataSourceOption } from './data_source_filter_group';
+import { getDataSourcesWithFields } from '../utils';
+
+export interface DataSourceMultiSeletableProps {
+ savedObjectsClient: SavedObjectsClientContract;
+ notifications: ToastsStart;
+ onSelectedDataSources: (dataSources: SelectedDataSourceOption[]) => void;
+ hideLocalCluster: boolean;
+ fullWidth: boolean;
+}
+
+interface DataSourceMultiSeletableState {
+ dataSourceOptions: SelectedDataSourceOption[];
+ selectedOptions: SelectedDataSourceOption[];
+}
+
+export class DataSourceMultiSelectable extends React.Component<
+ DataSourceMultiSeletableProps,
+ DataSourceMultiSeletableState
+> {
+ private _isMounted: boolean = false;
+
+ constructor(props: DataSourceMultiSeletableProps) {
+ super(props);
+
+ this.state = {
+ dataSourceOptions: [],
+ selectedOptions: [],
+ };
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ async componentDidMount() {
+ this._isMounted = true;
+ getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type'])
+ .then((fetchedDataSources) => {
+ if (fetchedDataSources?.length) {
+ // all data sources are selected by default on initial page load
+ const selectedOptions: SelectedDataSourceOption[] = fetchedDataSources.map(
+ (dataSource) => ({
+ id: dataSource.id,
+ label: dataSource.attributes?.title || '',
+ checked: 'on',
+ visible: true,
+ })
+ );
+
+ if (!this.props.hideLocalCluster) {
+ selectedOptions.unshift({
+ id: '',
+ label: 'Local cluster',
+ checked: 'on',
+ visible: true,
+ });
+ }
+
+ if (!this._isMounted) return;
+ this.setState({
+ ...this.state,
+ selectedOptions,
+ });
+ }
+ })
+ .catch(() => {
+ this.props.notifications.addWarning(
+ i18n.translate('dataSource.fetchDataSourceError', {
+ defaultMessage: 'Unable to fetch existing data sources',
+ })
+ );
+ });
+ }
+
+ onChange(selectedOptions: SelectedDataSourceOption[]) {
+ if (!this._isMounted) return;
+ this.setState({
+ selectedOptions,
+ });
+ this.props.onSelectedDataSources(selectedOptions.filter((option) => option.checked === 'on'));
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/index.ts b/src/plugins/data_source_management/public/components/data_source_multi_selectable/index.ts
new file mode 100644
index 000000000000..440cbfdc8202
--- /dev/null
+++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { DataSourceMultiSelectable } from './data_source_multi_selectable';
diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts
index 8ffda8701f68..471792ddd726 100644
--- a/src/plugins/data_source_management/public/index.ts
+++ b/src/plugins/data_source_management/public/index.ts
@@ -21,4 +21,5 @@ export {
DataSourceAggregatedViewConfig,
DataSourceViewConfig,
DataSourceMenuProps,
+ DataSourceMultiSelectableConfig,
} from './components/data_source_menu';