diff --git a/changelogs/fragments/6827.yml b/changelogs/fragments/6827.yml
new file mode 100644
index 000000000000..ce47fd32b7bd
--- /dev/null
+++ b/changelogs/fragments/6827.yml
@@ -0,0 +1,2 @@
+feat:
+- Add data source selection service to support storing and getting selected data source updates ([#6827](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6827))
\ No newline at end of file
diff --git a/examples/multiple_data_source_examples/public/components/data_source_view_example.tsx b/examples/multiple_data_source_examples/public/components/data_source_view_example.tsx
index b80a65bb1b1a..c7a2e0b7d94a 100644
--- a/examples/multiple_data_source_examples/public/components/data_source_view_example.tsx
+++ b/examples/multiple_data_source_examples/public/components/data_source_view_example.tsx
@@ -18,6 +18,7 @@ import { CoreStart, MountPoint } from 'opensearch-dashboards/public';
import {
DataSourceManagementPluginSetup,
DataSourceViewConfig,
+ DataSourceSelectionService,
} from 'src/plugins/data_source_management/public';
import { ComponentProp } from './types';
import { COLUMNS } from './constants';
@@ -88,6 +89,7 @@ export const DataSourceViewExample = ({
setSelectedDataSources(ds);
},
}}
+ dataSourceSelection={new DataSourceSelectionService()}
/>
);
}, [setActionMenu, notifications, savedObjects]);
diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap
index 23fee253ce08..d9084c4c76ec 100644
--- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap
+++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/__snapshots__/data_source_aggregated_view.test.tsx.snap
@@ -950,6 +950,109 @@ exports[`DataSourceAggregatedView error state test no matter hide local cluster
`;
+exports[`DataSourceAggregatedView: dataSourceSelection) should render normally and call selectDataSource 1`] = `
+
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="dataSourceSViewContextMenuPopover"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="none"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false) should render normally with local cluster and active selections configured 1`] = `
{
let component: ShallowWrapper, React.Component<{}, {}, any>>;
@@ -35,6 +36,7 @@ describe('DataSourceAggregatedView: read all view (displayAllCompatibleDataSourc
const { toasts } = notificationServiceMock.createStartContract();
const uiSettings = uiSettingsServiceMock.createStartContract();
const application = applicationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
beforeEach(() => {
@@ -44,6 +46,7 @@ describe('DataSourceAggregatedView: read all view (displayAllCompatibleDataSourc
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
mockUiSettingsCalls(uiSettings, 'get', 'test1');
jest.spyOn(utils, 'getApplication').mockReturnValue(application);
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
it.each([
@@ -163,6 +166,7 @@ describe('DataSourceAggregatedView: read active view (displayAllCompatibleDataSo
let client: SavedObjectsClientContract;
const { toasts } = notificationServiceMock.createStartContract();
const uiSettings = uiSettingsServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
beforeEach(() => {
@@ -171,6 +175,7 @@ describe('DataSourceAggregatedView: read active view (displayAllCompatibleDataSo
} as any;
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
mockUiSettingsCalls(uiSettings, 'get', 'test1');
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
it.each([
@@ -284,6 +289,7 @@ describe('DataSourceAggregatedView empty state test with local cluster hiding',
const { toasts } = notificationServiceMock.createStartContract();
const uiSettings = uiSettingsServiceMock.createStartContract();
const application = applicationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
beforeEach(() => {
@@ -293,6 +299,7 @@ describe('DataSourceAggregatedView empty state test with local cluster hiding',
mockResponseForSavedObjectsCalls(client, 'find', {});
mockUiSettingsCalls(uiSettings, 'get', 'test1');
jest.spyOn(utils, 'getApplication').mockReturnValue(application);
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
afterEach(() => {
@@ -369,6 +376,7 @@ describe('DataSourceAggregatedView empty state test due to filter out with local
const { toasts } = notificationServiceMock.createStartContract();
const uiSettings = uiSettingsServiceMock.createStartContract();
const application = applicationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
beforeEach(() => {
@@ -378,6 +386,7 @@ describe('DataSourceAggregatedView empty state test due to filter out with local
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
mockUiSettingsCalls(uiSettings, 'get', 'test1');
jest.spyOn(utils, 'getApplication').mockReturnValue(application);
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
afterEach(() => {
@@ -439,6 +448,7 @@ describe('DataSourceAggregatedView error state test no matter hide local cluster
const { toasts } = notificationServiceMock.createStartContract();
const uiSettings = uiSettingsServiceMock.createStartContract();
const application = applicationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
beforeEach(() => {
@@ -448,6 +458,7 @@ describe('DataSourceAggregatedView error state test no matter hide local cluster
mockErrorResponseForSavedObjectsCalls(client, 'find');
mockUiSettingsCalls(uiSettings, 'get', 'test1');
jest.spyOn(utils, 'getApplication').mockReturnValue(application);
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
afterEach(() => {
@@ -514,6 +525,7 @@ describe('DataSourceAggregatedView error state test no matter hide local cluster
describe('DataSourceAggregatedView warning messages', () => {
const client = {} as any;
const uiSettings = uiSettingsServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
const nextTick = () => new Promise((res) => process.nextTick(res));
let toasts: IToasts;
const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`;
@@ -522,6 +534,7 @@ describe('DataSourceAggregatedView warning messages', () => {
beforeEach(() => {
toasts = notificationServiceMock.createStartContract().toasts;
mockUiSettingsCalls(uiSettings, 'get', 'test1');
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
});
it.each([
@@ -571,3 +584,47 @@ describe('DataSourceAggregatedView warning messages', () => {
}
);
});
+
+describe('DataSourceAggregatedView: dataSourceSelection)', () => {
+ let client: SavedObjectsClientContract;
+ const { toasts } = notificationServiceMock.createStartContract();
+ const uiSettings = uiSettingsServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
+ dataSourceSelection.selectDataSource = jest.fn();
+ const nextTick = () => new Promise((res) => process.nextTick(res));
+ const activeDataSourceIds = ['test1', 'test2'];
+ const selectedOptions = [
+ { checked: 'on', disabled: true, id: 'test1', label: 'test1' },
+ { checked: 'on', disabled: true, id: 'test2', label: 'test2' },
+ ];
+ const componentId = 'component-id';
+ beforeEach(() => {
+ client = {
+ find: jest.fn().mockResolvedValue([]),
+ } as any;
+ mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
+ mockUiSettingsCalls(uiSettings, 'get', 'test1');
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelection);
+ jest.spyOn(utils, 'generateComponentId').mockReturnValue(componentId);
+ });
+
+ it('should render normally and call selectDataSource', async () => {
+ const component = shallow(
+
+ );
+
+ // Should render normally
+ expect(component).toMatchSnapshot();
+ await nextTick();
+
+ expect(dataSourceSelection.selectDataSource).toHaveBeenCalledWith(componentId, selectedOptions);
+ });
+});
diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx
index 8d8cd2ed0ef9..8eda8143c805 100644
--- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx
@@ -16,6 +16,8 @@ import {
getDataSourcesWithFields,
handleDataSourceFetchError,
handleNoAvailableDataSourceError,
+ generateComponentId,
+ getDataSourceSelection,
} from '../utils';
import { SavedObject } from '../../../../../core/public';
import { DataSourceAttributes } from '../../types';
@@ -46,6 +48,7 @@ interface DataSourceAggregatedViewState extends DataSourceBaseState {
switchChecked: boolean;
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
+ componentId: string;
}
interface DataSourceOptionDisplay extends DataSourceOption {
@@ -70,11 +73,13 @@ export class DataSourceAggregatedView extends React.Component<
switchChecked: false,
defaultDataSource: null,
incompatibleDataSourcesExist: false,
+ componentId: generateComponentId(),
};
}
componentWillUnmount() {
this._isMounted = false;
+ getDataSourceSelection().remove(this.state.componentId);
}
onDataSourcesClick() {
@@ -188,7 +193,11 @@ export class DataSourceAggregatedView extends React.Component<
});
}
- const numSelectedItems = items.filter((item) => item.checked === 'on').length;
+ const selectedItems = items.filter((item) => item.checked === 'on');
+ // For read-only cases, also need to set default selected result.
+ getDataSourceSelection().selectDataSource(this.state.componentId, selectedItems);
+
+ const numSelectedItems = selectedItems.length;
const titleComponent = (
{
let client: SavedObjectsClientContract;
const notifications = notificationServiceMock.createStartContract();
const { uiSettings } = coreMock.createSetup();
+ const dataSourceSelection = new DataSourceSelectionService();
beforeAll(() => {
jest
@@ -47,6 +49,8 @@ describe('create data source menu', () => {
spyOn(utils, 'getApplication').and.returnValue({ id: 'test2' });
spyOn(utils, 'getUiSettings').and.returnValue(uiSettings);
spyOn(utils, 'getHideLocalCluster').and.returnValue({ enabled: true });
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
+
const TestComponent = createDataSourceMenu();
const component = render();
@@ -74,6 +78,7 @@ describe('create data source menu', () => {
spyOn(utils, 'getApplication').and.returnValue({ id: 'test2' });
spyOn(utils, 'getUiSettings').and.returnValue(uiSettings);
spyOn(utils, 'getHideLocalCluster').and.returnValue({ enabled: true });
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
const TestComponent = createDataSourceMenu();
await act(async () => {
component = render();
@@ -98,6 +103,7 @@ describe('when setMenuMountPoint is provided', () => {
let client: SavedObjectsClientContract;
const notifications = notificationServiceMock.createStartContract();
const { uiSettings } = coreMock.createSetup();
+ const dataSourceSelection = new DataSourceSelectionService();
const refresh = () => {
new Promise(async (resolve) => {
@@ -141,6 +147,7 @@ describe('when setMenuMountPoint is provided', () => {
spyOn(utils, 'getApplication').and.returnValue({ id: 'test2' });
spyOn(utils, 'getUiSettings').and.returnValue(uiSettings);
spyOn(utils, 'getHideLocalCluster').and.returnValue({ enabled: true });
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
const TestComponent = createDataSourceMenu();
const component = render();
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
index a628a1bf2a15..1d053c0449b7 100644
--- 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
@@ -14,7 +14,9 @@ export function createDataSourceMenu() {
const application = getApplication();
const uiSettings = getUiSettings();
const hideLocalCluster = getHideLocalCluster().enabled;
- return (props: DataSourceMenuProps) => {
+ return (
+ props: Omit, 'uiSettings' | 'hideLocalCluster' | 'application'>
+ ) => {
if (props.setMenuMountPoint) {
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
index c8325a5b14b4..6ed297f9f014 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
@@ -11,6 +11,7 @@ import { DataSourceMenu } from './data_source_menu';
import { render } from '@testing-library/react';
import { DataSourceComponentType } from './types';
import * as utils from '../utils';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
describe('DataSourceMenu', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
@@ -18,11 +19,13 @@ describe('DataSourceMenu', () => {
let client: SavedObjectsClientContract;
const notifications = notificationServiceMock.createStartContract();
const application = applicationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
beforeEach(() => {
client = {
find: jest.fn().mockResolvedValue([]),
} as any;
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
});
it('should render data source selectable only with local cluster not hidden', () => {
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
index 7a9f1f96ed0c..5ccf6d5d9dce 100644
--- 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
@@ -4,7 +4,7 @@
*/
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
-import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks';
+import { notificationServiceMock } from '../../../../../core/public/mocks';
import {
getDataSourcesWithFieldsResponse,
mockResponseForSavedObjectsCalls,
@@ -14,6 +14,8 @@ import { ShallowWrapper, mount, shallow } from 'enzyme';
import { DataSourceMultiSelectable } from './data_source_multi_selectable';
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
+import * as utils from '../utils';
describe('DataSourceMultiSelectable', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
@@ -23,6 +25,7 @@ describe('DataSourceMultiSelectable', () => {
const nextTick = () => new Promise((res) => process.nextTick(res));
const mockedContext = mockManagementPlugin.createDataSourceManagementContext();
const uiSettings = mockedContext.uiSettings;
+ const dataSourceSelection = new DataSourceSelectionService();
beforeEach(() => {
jest.clearAllMocks();
@@ -31,6 +34,7 @@ describe('DataSourceMultiSelectable', () => {
find: jest.fn().mockResolvedValue([]),
} as any;
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
});
it('should render normally with local cluster not hidden', () => {
@@ -185,4 +189,39 @@ describe('DataSourceMultiSelectable', () => {
expect(wrapper.state('selectedOptions')).toHaveLength(1);
expect(wrapper.state('showEmptyState')).toBe(false);
});
+
+ it('should call dataSourceSelection selectDataSource when selecting', async () => {
+ const dataSourceSelectionMock = new DataSourceSelectionService();
+ const componentId = 'component-id';
+ const selectedOptions = [
+ { checked: 'on', id: 'test1', label: 'test1', visible: true },
+ { checked: 'on', id: 'test2', label: 'test2', visible: true },
+ { checked: 'on', id: 'test3', label: 'test3', visible: true },
+ ];
+ dataSourceSelectionMock.selectDataSource = jest.fn();
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelectionMock);
+ jest.spyOn(utils, 'generateComponentId').mockReturnValue(componentId);
+
+ const container = render(
+
+ );
+
+ await component.instance().componentDidMount!();
+ expect(dataSourceSelectionMock.selectDataSource).toHaveBeenCalledWith(
+ componentId,
+ selectedOptions
+ );
+
+ const button = await container.findByTestId('dataSourceFilterGroupButton');
+ button.click();
+ fireEvent.click(screen.getByText('Deselect all'));
+
+ expect(dataSourceSelectionMock.selectDataSource).toHaveBeenCalledWith(componentId, []);
+ });
});
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
index 481df093d741..995a5f3ca731 100644
--- 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
@@ -16,6 +16,8 @@ import {
getDataSourcesWithFields,
handleDataSourceFetchError,
handleNoAvailableDataSourceError,
+ generateComponentId,
+ getDataSourceSelection,
} from '../utils';
import { DataSourceBaseState } from '../data_source_menu/types';
import { DataSourceErrorMenu } from '../data_source_error_menu';
@@ -35,6 +37,7 @@ interface DataSourceMultiSeletableState extends DataSourceBaseState {
selectedOptions: SelectedDataSourceOption[];
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
+ componentId: string;
}
export class DataSourceMultiSelectable extends React.Component<
@@ -53,11 +56,21 @@ export class DataSourceMultiSelectable extends React.Component<
showEmptyState: false,
showError: false,
incompatibleDataSourcesExist: false,
+ componentId: generateComponentId(),
};
}
componentWillUnmount() {
this._isMounted = false;
+ const { componentId } = this.state;
+ getDataSourceSelection().remove(componentId);
+ }
+
+ onSelectedDataSources(dataSources: SelectedDataSourceOption[]) {
+ getDataSourceSelection().selectDataSource(this.state.componentId, dataSources);
+ if (this.props.onSelectedDataSources) {
+ this.props.onSelectedDataSources(dataSources);
+ }
}
async componentDidMount() {
@@ -96,7 +109,7 @@ export class DataSourceMultiSelectable extends React.Component<
changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length),
notifications: this.props.notifications,
application: this.props.application,
- callback: this.props.onSelectedDataSources,
+ callback: this.onSelectedDataSources.bind(this),
incompatibleDataSourcesExist: !!fetchedDataSources?.length,
});
return;
@@ -108,12 +121,12 @@ export class DataSourceMultiSelectable extends React.Component<
defaultDataSource,
});
- this.props.onSelectedDataSources(selectedOptions);
+ this.onSelectedDataSources(selectedOptions);
} catch (error) {
handleDataSourceFetchError(
this.onError.bind(this),
this.props.notifications,
- this.props.onSelectedDataSources
+ this.onSelectedDataSources.bind(this)
);
}
}
@@ -131,7 +144,7 @@ export class DataSourceMultiSelectable extends React.Component<
this.setState({
selectedOptions,
});
- this.props.onSelectedDataSources(selectedOptions.filter((option) => option.checked === 'on'));
+ this.onSelectedDataSources(selectedOptions.filter((option) => option.checked === 'on'));
}
render() {
diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx
index 6521aaddb258..a880a99ed71a 100644
--- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx
@@ -19,21 +19,28 @@ import {
NO_COMPATIBLE_DATASOURCES_MESSAGE,
ADD_COMPATIBLE_DATASOURCES_MESSAGE,
} from '../constants';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
+
+const mockGeneratedComponentId = 'component-id';
+jest.mock('uuid', () => ({ v4: () => mockGeneratedComponentId }));
describe('DataSourceSelectable', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
let client: SavedObjectsClientContract;
+
const { toasts } = notificationServiceMock.createStartContract();
const nextTick = () => new Promise((res) => process.nextTick(res));
const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`;
const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`;
+ const dataSourceSelection = new DataSourceSelectionService();
beforeEach(() => {
client = {
find: jest.fn().mockResolvedValue([]),
} as any;
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
});
it('should render normally with local cluster not hidden', () => {
@@ -138,6 +145,7 @@ describe('DataSourceSelectable', () => {
containerInstance.onChange([{ id: 'test2', label: 'test2' }]);
expect(onSelectedDataSource).toBeCalledTimes(1);
expect(containerInstance.state).toEqual({
+ componentId: mockGeneratedComponentId,
dataSourceOptions: [
{
id: 'test2',
@@ -159,6 +167,7 @@ describe('DataSourceSelectable', () => {
containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]);
expect(containerInstance.state).toEqual({
+ componentId: mockGeneratedComponentId,
dataSourceOptions: [
{
checked: 'on',
@@ -331,6 +340,7 @@ describe('DataSourceSelectable', () => {
await nextTick();
const containerInstance = container.instance();
expect(containerInstance.state).toEqual({
+ componentId: mockGeneratedComponentId,
dataSourceOptions: [
{
id: 'test1',
@@ -380,6 +390,7 @@ describe('DataSourceSelectable', () => {
expect(onSelectedDataSource).toBeCalledWith([]);
expect(containerInstance.state).toEqual({
+ componentId: mockGeneratedComponentId,
dataSourceOptions: [],
defaultDataSource: null,
isPopoverOpen: false,
@@ -391,6 +402,7 @@ describe('DataSourceSelectable', () => {
containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]);
expect(containerInstance.state).toEqual({
+ componentId: mockGeneratedComponentId,
dataSourceOptions: [
{
checked: 'on',
@@ -464,4 +476,30 @@ describe('DataSourceSelectable', () => {
expect(onSelectedDataSource).toBeCalledWith([]);
}
);
+
+ it('should call dataSourceSelection selectDataSource when selecting', async () => {
+ spyOn(utils, 'getDefaultDataSource').and.returnValue([{ id: 'test2', label: 'test2' }]);
+ const dataSourceSelectionMock = new DataSourceSelectionService();
+ const componentId = 'component-id';
+ const selectedOptions = [{ id: 'test2', label: 'test2' }];
+ dataSourceSelectionMock.selectDataSource = jest.fn();
+ jest.spyOn(utils, 'getDataSourceSelection').mockReturnValue(dataSourceSelectionMock);
+ jest.spyOn(utils, 'generateComponentId').mockReturnValue(componentId);
+ mount(
+ ds.attributes.auth.type !== AuthType.NoAuth}
+ />
+ );
+ await nextTick();
+ expect(dataSourceSelectionMock.selectDataSource).toHaveBeenCalledWith(
+ componentId,
+ selectedOptions
+ );
+ });
});
diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx
index fd1c685676f0..b181da74e542 100644
--- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx
@@ -24,6 +24,8 @@ import {
getFilteredDataSources,
handleDataSourceFetchError,
handleNoAvailableDataSourceError,
+ generateComponentId,
+ getDataSourceSelection,
} from '../utils';
import { LocalCluster } from '../data_source_selector/data_source_selector';
import { SavedObject } from '../../../../../core/public';
@@ -57,6 +59,7 @@ interface DataSourceSelectableState extends DataSourceBaseState {
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
selectedOption?: DataSourceOption[];
+ componentId: string;
}
export class DataSourceSelectable extends React.Component<
@@ -76,6 +79,7 @@ export class DataSourceSelectable extends React.Component<
showEmptyState: false,
showError: false,
incompatibleDataSourcesExist: false,
+ componentId: generateComponentId(),
};
this.onChange.bind(this);
@@ -83,6 +87,7 @@ export class DataSourceSelectable extends React.Component<
componentWillUnmount() {
this._isMounted = false;
+ getDataSourceSelection().remove(this.state.componentId);
}
onClick() {
@@ -93,6 +98,11 @@ export class DataSourceSelectable extends React.Component<
this.setState({ ...this.state, isPopoverOpen: false });
}
+ onSelectedDataSources(dataSources: DataSourceOption[]) {
+ getDataSourceSelection().selectDataSource(this.state.componentId, dataSources);
+ this.props.onSelectedDataSources(dataSources);
+ }
+
// Update the checked status of the selected data source.
getUpdatedDataSourceOptions(
selectedDataSourceId: string,
@@ -119,7 +129,7 @@ export class DataSourceSelectable extends React.Component<
selectedOption: [],
defaultDataSource,
});
- this.props.onSelectedDataSources([]);
+ this.onSelectedDataSources([]);
return;
}
const updatedDataSourceOptions: DataSourceOption[] = this.getUpdatedDataSourceOptions(
@@ -132,7 +142,7 @@ export class DataSourceSelectable extends React.Component<
selectedOption: [{ id, label: dsOption.label }],
defaultDataSource,
});
- this.props.onSelectedDataSources([{ id, label: dsOption.label }]);
+ this.onSelectedDataSources([{ id, label: dsOption.label }]);
}
handleDefaultDataSource(dataSourceOptions: DataSourceOption[], defaultDataSource: string | null) {
@@ -146,7 +156,7 @@ export class DataSourceSelectable extends React.Component<
// no active option, show warning
if (selectedDataSource.length === 0) {
this.props.notifications.addWarning('No connected data source available.');
- this.props.onSelectedDataSources([]);
+ this.onSelectedDataSources([]);
return;
}
@@ -162,7 +172,7 @@ export class DataSourceSelectable extends React.Component<
defaultDataSource,
});
- this.props.onSelectedDataSources(selectedDataSource);
+ this.onSelectedDataSources(selectedDataSource);
}
async componentDidMount() {
@@ -193,7 +203,7 @@ export class DataSourceSelectable extends React.Component<
changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length),
notifications: this.props.notifications,
application: this.props.application,
- callback: this.props.onSelectedDataSources,
+ callback: this.onSelectedDataSources.bind(this),
incompatibleDataSourcesExist: !!fetchedDataSources?.length,
});
return;
@@ -212,7 +222,7 @@ export class DataSourceSelectable extends React.Component<
handleDataSourceFetchError(
this.onError.bind(this),
this.props.notifications,
- this.props.onSelectedDataSources
+ this.onSelectedDataSources.bind(this)
);
}
}
@@ -237,9 +247,7 @@ export class DataSourceSelectable extends React.Component<
isPopoverOpen: false,
});
- this.props.onSelectedDataSources([
- { id: selectedDataSource.id!, label: selectedDataSource.label },
- ]);
+ this.onSelectedDataSources([{ id: selectedDataSource.id!, label: selectedDataSource.label }]);
}
}
diff --git a/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.test.tsx b/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.test.tsx
index 9049c6767c7c..06bd223d60eb 100644
--- a/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.test.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.test.tsx
@@ -12,6 +12,8 @@ import {
mockDataSourcePluginSetupWithHideLocalCluster,
mockDataSourcePluginSetupWithShowLocalCluster,
} from '../../mocks';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
+import * as utils from '../utils';
describe('create data source selector', () => {
let client: SavedObjectsClientContract;
@@ -33,6 +35,9 @@ describe('create data source selector', () => {
hideLocalCluster: false,
fullWidth: false,
};
+ const dataSourceSelection = new DataSourceSelectionService();
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
+
const TestComponent = createDataSourceSelector(
uiSettings,
mockDataSourcePluginSetupWithHideLocalCluster
@@ -56,6 +61,9 @@ describe('create data source selector', () => {
hideLocalCluster: true,
fullWidth: false,
};
+ const dataSourceSelection = new DataSourceSelectionService();
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
+
const TestComponent = createDataSourceSelector(
uiSettings,
mockDataSourcePluginSetupWithShowLocalCluster
diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx
index 4f8df2542059..a99ccb4d5908 100644
--- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx
@@ -15,12 +15,15 @@ import {
} from '../../mocks';
import { AuthType } from 'src/plugins/data_source/common/data_sources';
import { EuiComboBox } from '@elastic/eui';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
+import * as utils from '../utils';
describe('DataSourceSelector', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
let client: SavedObjectsClientContract;
const { toasts } = notificationServiceMock.createStartContract();
+ const dataSourceSelection = new DataSourceSelectionService();
beforeEach(() => {
jest.clearAllMocks();
@@ -30,6 +33,7 @@ describe('DataSourceSelector', () => {
});
it('should render normally with local cluster not hidden', () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should render normally with local cluster is hidden', () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
const nextTick = () => new Promise((res) => process.nextTick(res));
const mockedContext = mockManagementPlugin.createDataSourceManagementContext();
const uiSettings = mockedContext.uiSettings;
+ const dataSourceSelection = new DataSourceSelectionService();
beforeEach(async () => {
jest.clearAllMocks();
@@ -88,6 +94,7 @@ describe('DataSourceSelector: check dataSource options', () => {
});
it('should always place local cluster option as the first option when local cluster not hidden', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should hide prepend if removePrepend is true', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should show custom placeholder text if configured', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should filter options if configured', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should return empty options if filter out all options and hide local cluster', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should get default datasource if uiSettings exists', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue('test1');
component = shallow(
{
});
it('should not render options with default badge when id does not matches defaultDataSource', () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
const nextTick = () => new Promise((res) => process.nextTick(res));
const mockedContext = mockManagementPlugin.createDataSourceManagementContext();
const uiSettings = mockedContext.uiSettings;
+ const dataSourceSelection = new DataSourceSelectionService();
const getMockedDataSourceOptions = () => {
return getDataSourcesWithFieldsResponse.savedObjects.map((response) => {
return { id: response.id, label: response.attributes.title };
@@ -245,6 +259,7 @@ describe('DataSourceSelector: check defaultOption behavior', () => {
// When defaultOption is undefined
it('should render defaultDataSource as the selected option', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue('test1');
component = shallow(
{
});
it('should render Local Cluster as the selected option when hideLocalCluster is false', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue(null);
component = shallow(
{
});
it('should render random datasource as the selected option if defaultDataSource and Local Cluster are not present', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue(null);
component = shallow(
{
});
it('should return toast', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue(null);
component = shallow(
{
// When defaultOption is []
it('should render placeholder and all options when Local Cluster is not hidden', async () => {
spyOn(uiSettings, 'get').and.returnValue('test1');
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
});
it('should render placeholder and all options when Local Cluster is hidden', async () => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue('test1');
component = shallow(
{
id: 'non-existent-id',
},
])('should all throw a toast warning when the available dataSources is empty', async ({ id }) => {
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
spyOn(uiSettings, 'get').and.returnValue('test1');
component = shallow(
{
},
])('should all throw a toast warning when the id is filtered out', async ({ id }) => {
spyOn(uiSettings, 'get').and.returnValue('test1');
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
dataSourceFilter={(dataSource) => {
return dataSource.attributes.title !== id;
}}
+ dataSourceSelection={dataSourceSelection}
// @ts-expect-error
defaultOption={[{ id }]}
/>
@@ -490,6 +513,7 @@ describe('DataSourceSelector: check defaultOption behavior', () => {
'should handle selectedOption correctly when defaultOption = [{id}]',
async ({ id, error, selectedOption }) => {
spyOn(uiSettings, 'get').and.returnValue('test1');
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
component = shallow(
{
hideLocalCluster={false}
fullWidth={false}
uiSettings={uiSettings}
+ dataSourceSelection={dataSourceSelection}
// @ts-expect-error
defaultOption={[{ id }]}
/>
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 8d2a90943b9f..5d9c1b17fbd9 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
@@ -8,7 +8,13 @@ import { i18n } from '@osd/i18n';
import { EuiComboBox } from '@elastic/eui';
import { SavedObjectsClientContract, ToastsStart, SavedObject } from 'opensearch-dashboards/public';
import { IUiSettingsClient } from 'src/core/public';
-import { getDataSourcesWithFields, getDefaultDataSource, getFilteredDataSources } from '../utils';
+import {
+ getDataSourcesWithFields,
+ getDefaultDataSource,
+ getFilteredDataSources,
+ generateComponentId,
+ getDataSourceSelection,
+} from '../utils';
import { DataSourceAttributes } from '../../types';
import { DataSourceItem } from '../data_source_item';
import './data_source_selector.scss';
@@ -42,6 +48,7 @@ interface DataSourceSelectorState {
allDataSources: Array>;
defaultDataSource: string | null;
dataSourceOptions: DataSourceOption[];
+ componentId: string;
}
export class DataSourceSelector extends React.Component<
@@ -58,11 +65,18 @@ export class DataSourceSelector extends React.Component<
defaultDataSource: '',
selectedOption: this.props.hideLocalCluster ? [] : [LocalCluster],
dataSourceOptions: [],
+ componentId: generateComponentId(),
};
}
componentWillUnmount() {
this._isMounted = false;
+ getDataSourceSelection().remove(this.state.componentId);
+ }
+
+ onSelectedDataSource(dataSource: DataSourceOption[]) {
+ getDataSourceSelection().selectDataSource(this.state.componentId, dataSource);
+ this.props.onSelectedDataSource(dataSource);
}
handleSelectedOption(
@@ -90,7 +104,7 @@ export class DataSourceSelector extends React.Component<
defaultDataSource,
allDataSources,
});
- this.props.onSelectedDataSource(selectedOption);
+ this.onSelectedDataSource(selectedOption);
}
handleDefaultDataSource(
@@ -108,7 +122,7 @@ export class DataSourceSelector extends React.Component<
// No active option, did not find valid option
if (selectedDataSource.length === 0) {
this.props.notifications.addWarning('No connected data source available.');
- this.props.onSelectedDataSource([]);
+ this.onSelectedDataSource([]);
return;
}
@@ -119,7 +133,7 @@ export class DataSourceSelector extends React.Component<
defaultDataSource,
allDataSources,
});
- this.props.onSelectedDataSource(selectedDataSource);
+ this.onSelectedDataSource(selectedDataSource);
}
async componentDidMount() {
@@ -146,7 +160,7 @@ export class DataSourceSelector extends React.Component<
// 4. Error state if filter filters out everything
if (!dataSourceOptions.length) {
this.props.notifications.addWarning('No connected data source available.');
- this.props.onSelectedDataSource([]);
+ this.onSelectedDataSource([]);
return;
}
@@ -185,7 +199,7 @@ export class DataSourceSelector extends React.Component<
this.setState({
selectedOption: e,
});
- this.props.onSelectedDataSource(e);
+ this.onSelectedDataSource(e);
}
render() {
diff --git a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.test.tsx b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.test.tsx
index 07c36e414141..a77b972ee00b 100644
--- a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.test.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.test.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ShallowWrapper, shallow, mount } from 'enzyme';
+import { ShallowWrapper, shallow } from 'enzyme';
import React from 'react';
import { DataSourceView } from './data_source_view';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
@@ -11,10 +11,12 @@ import { notificationServiceMock } from '../../../../../core/public/mocks';
import { getSingleDataSourceResponse, mockResponseForSavedObjectsCalls } from '../../mocks';
import { render } from '@testing-library/react';
import * as utils from '../utils';
+import { DataSourceSelectionService } from '../../service/data_source_selection_service';
describe('DataSourceView', () => {
let component: ShallowWrapper, React.Component<{}, {}, any>>;
let client: SavedObjectsClientContract;
+ const dataSourceSelection = new DataSourceSelectionService();
const { toasts } = notificationServiceMock.createStartContract();
beforeEach(() => {
@@ -23,6 +25,7 @@ describe('DataSourceView', () => {
get: jest.fn().mockResolvedValue([]),
} as any;
mockResponseForSavedObjectsCalls(client, 'get', getSingleDataSourceResponse);
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
});
it('should render normally with local cluster not hidden', () => {
diff --git a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx
index f0d53ab92ddf..6080d91d0e4f 100644
--- a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx
+++ b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx
@@ -4,14 +4,7 @@
*/
import React from 'react';
-import { i18n } from '@osd/i18n';
-import {
- EuiPopover,
- EuiButtonEmpty,
- EuiContextMenuPanel,
- EuiPanel,
- EuiSelectable,
-} from '@elastic/eui';
+import { EuiPopover, EuiContextMenuPanel, EuiPanel, EuiSelectable } from '@elastic/eui';
import {
SavedObjectsClientContract,
ToastsStart,
@@ -20,7 +13,12 @@ import {
import { IUiSettingsClient } from 'src/core/public';
import { DataSourceBaseState, DataSourceOption } from '../data_source_menu/types';
import { DataSourceErrorMenu } from '../data_source_error_menu';
-import { getDataSourceById, handleDataSourceFetchError } from '../utils';
+import {
+ getDataSourceById,
+ handleDataSourceFetchError,
+ generateComponentId,
+ getDataSourceSelection,
+} from '../utils';
import { DataSourceDropDownHeader } from '../drop_down_header';
import { DataSourceItem } from '../data_source_item';
import { LocalCluster } from '../constants';
@@ -43,6 +41,7 @@ interface DataSourceViewState extends DataSourceBaseState {
selectedOption: DataSourceOption[];
isPopoverOpen: boolean;
defaultDataSource: string | null;
+ componentId: string;
}
export class DataSourceView extends React.Component {
@@ -57,11 +56,13 @@ export class DataSourceView extends React.Component {
expect(result).toEqual([{ id: '1', label: 'DataSource 1' }]);
});
});
+
+ describe('getDataSourceSelection and setDataSourceSelection', () => {
+ it('should not throw error and return default fallback dataSourceSelection if value is not set', () => {
+ const result = getDataSourceSelection();
+ expect(result).toEqual(defaultDataSourceSelection);
+ });
+
+ it('should return value normally if value is set', () => {
+ const dataSourceSelection = new DataSourceSelectionService();
+ setDataSourceSelection(dataSourceSelection);
+ const result = getDataSourceSelection();
+ expect(result).toEqual(dataSourceSelection);
+ });
+ });
});
diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts
index 7a4ec06d5cfb..ac048db0587b 100644
--- a/src/plugins/data_source_management/public/components/utils.ts
+++ b/src/plugins/data_source_management/public/components/utils.ts
@@ -13,6 +13,7 @@ import {
CoreStart,
} from 'src/core/public';
import { deepFreeze } from '@osd/std';
+import uuid from 'uuid';
import {
DataSourceAttributes,
DataSourceTableItem,
@@ -31,6 +32,10 @@ import {
NO_COMPATIBLE_DATASOURCES_MESSAGE,
NO_DATASOURCES_CONNECTED_MESSAGE,
} from './constants';
+import {
+ DataSourceSelectionService,
+ defaultDataSourceSelection,
+} from '../service/data_source_selection_service';
export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) {
return savedObjectsClient
@@ -341,3 +346,24 @@ export interface HideLocalCluster {
export const [getHideLocalCluster, setHideLocalCluster] = createGetterSetter(
'HideLocalCluster'
);
+
+// This will maintain an unified data source selection instance among components and export it to other plugin.
+const [getDataSourceSelectionInstance, setDataSourceSelection] = createGetterSetter<
+ DataSourceSelectionService
+>('DataSourceSelectionService');
+
+const getDataSourceSelection = () => {
+ try {
+ // Usually set will be executed in the setup of DSM.
+ return getDataSourceSelectionInstance();
+ } catch (e) {
+ // Since createGetterSetter doesn't support default value and will throw error if not found.
+ // As dataSourceSelection isn't main part of data selector, will use a default to fallback safely.
+ return defaultDataSourceSelection;
+ }
+};
+export { getDataSourceSelection, setDataSourceSelection };
+
+export const generateComponentId = () => {
+ return uuid.v4();
+};
diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts
index 0cc7370d2cdc..2607f5b8c7f9 100644
--- a/src/plugins/data_source_management/public/index.ts
+++ b/src/plugins/data_source_management/public/index.ts
@@ -24,3 +24,4 @@ export {
DataSourceMultiSelectableConfig,
createDataSourceMenu,
} from './components/data_source_menu';
+export { DataSourceSelectionService } from './service/data_source_selection_service';
diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts
index 7f0eb9760be2..2f08b942db15 100644
--- a/src/plugins/data_source_management/public/plugin.ts
+++ b/src/plugins/data_source_management/public/plugin.ts
@@ -21,7 +21,13 @@ import { noAuthCredentialAuthMethod, sigV4AuthMethod, usernamePasswordAuthMethod
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';
-import { setApplication, setHideLocalCluster, setUiSettings } from './components/utils';
+import {
+ setApplication,
+ setHideLocalCluster,
+ setUiSettings,
+ setDataSourceSelection,
+} from './components/utils';
+import { DataSourceSelectionService } from './service/data_source_selection_service';
export interface DataSourceManagementSetupDependencies {
management: ManagementSetup;
@@ -35,6 +41,7 @@ export interface DataSourceManagementPluginSetup {
DataSourceSelector: React.ComponentType;
getDataSourceMenu: () => React.ComponentType>;
};
+ dataSourceSelection: DataSourceSelectionService;
}
export interface DataSourceManagementPluginStart {
@@ -53,6 +60,7 @@ export class DataSourceManagementPlugin
> {
private started = false;
private authMethodsRegistry = new AuthenticationMethodRegistry();
+ private dataSourceSelection = new DataSourceSelectionService();
public setup(
core: CoreSetup,
@@ -104,9 +112,13 @@ export class DataSourceManagementPlugin
setHideLocalCluster({ enabled: dataSource.hideLocalCluster });
setUiSettings(uiSettings);
+ // This instance will be got in each data source selector component.
+ setDataSourceSelection(this.dataSourceSelection);
return {
registerAuthenticationMethod,
+ // Other plugins can get this instance from setupDeps and use to get selected data sources.
+ dataSourceSelection: this.dataSourceSelection,
ui: {
DataSourceSelector: createDataSourceSelector(uiSettings, dataSource),
getDataSourceMenu: () => createDataSourceMenu(),
diff --git a/src/plugins/data_source_management/public/service/data_source_selection_service.test.ts b/src/plugins/data_source_management/public/service/data_source_selection_service.test.ts
new file mode 100644
index 000000000000..65957379517f
--- /dev/null
+++ b/src/plugins/data_source_management/public/service/data_source_selection_service.test.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { DataSourceSelectionService } from './data_source_selection_service';
+import { generateComponentId } from '../components/utils';
+
+describe('DataSourceSelectionService service', () => {
+ it('basic set, get and remove', async () => {
+ const dataSourceSelection = new DataSourceSelectionService();
+ const id = generateComponentId();
+ const dataSource = { id: 'id', label: 'label' };
+ expect(dataSourceSelection.getSelectionValue().get(id)).toBe(undefined);
+ dataSourceSelection.selectDataSource(id, [dataSource]);
+ expect(dataSourceSelection.getSelectionValue().get(id)).toStrictEqual([dataSource]);
+ dataSourceSelection.remove(id);
+ expect(dataSourceSelection.getSelectionValue().get(id)).toBe(undefined);
+ });
+
+ it('multiple set and get', async () => {
+ const dataSourceSelection = new DataSourceSelectionService();
+ const id1 = generateComponentId();
+ const id2 = generateComponentId();
+
+ const dataSource = { id: 'id', label: 'label' };
+ expect(dataSourceSelection.getSelectionValue().get(id1)).toBe(undefined);
+ expect(dataSourceSelection.getSelectionValue().get(id2)).toBe(undefined);
+ dataSourceSelection.selectDataSource(id1, [dataSource]);
+ dataSourceSelection.selectDataSource(id2, [dataSource]);
+ expect(dataSourceSelection.getSelectionValue().get(id1)).toStrictEqual([dataSource]);
+ expect(dataSourceSelection.getSelectionValue().get(id2)).toStrictEqual([dataSource]);
+ dataSourceSelection.remove(id1);
+ expect(dataSourceSelection.getSelectionValue().get(id1)).toBe(undefined);
+ expect(dataSourceSelection.getSelectionValue().get(id2)).toStrictEqual([dataSource]);
+ });
+
+ it('support subscribing selected observable', (done) => {
+ const dataSourceSelection = new DataSourceSelectionService();
+ const selectedDataSource$ = dataSourceSelection.getSelection$();
+ const id = generateComponentId();
+ const dataSource = { id: 'id', label: 'label' };
+ dataSourceSelection.selectDataSource(id, [dataSource]);
+ selectedDataSource$.subscribe((newValue) => {
+ expect(newValue.get(id)).toStrictEqual([dataSource]);
+ done();
+ });
+ });
+});
diff --git a/src/plugins/data_source_management/public/service/data_source_selection_service.ts b/src/plugins/data_source_management/public/service/data_source_selection_service.ts
new file mode 100644
index 000000000000..560216d88c3a
--- /dev/null
+++ b/src/plugins/data_source_management/public/service/data_source_selection_service.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BehaviorSubject } from 'rxjs';
+import { DataSourceOption } from '../components/data_source_menu/types';
+
+export class DataSourceSelectionService {
+ private selectedDataSource$ = new BehaviorSubject(new Map());
+
+ public selectDataSource = (componentId: string, dataSource: DataSourceOption[]) => {
+ const newMap = new Map(this.selectedDataSource$.value);
+ newMap.set(componentId, dataSource);
+ this.selectedDataSource$.next(newMap);
+ };
+
+ public remove = (componentId: string) => {
+ const newMap = new Map(this.selectedDataSource$.value);
+ newMap.delete(componentId);
+ this.selectedDataSource$.next(newMap);
+ };
+
+ public getSelectionValue = () => {
+ return this.selectedDataSource$.value;
+ };
+
+ // Plugins can use returned subject to subscribe update.
+ public getSelection$ = () => {
+ return this.selectedDataSource$;
+ };
+}
+
+// This is an empty instance of DataSourceSelection for fallback.
+export const defaultDataSourceSelection = {
+ selectDataSource: () => {},
+ remove: () => {},
+ getSelectionValue: () => {},
+ getSelection$: () => {},
+};
diff --git a/src/plugins/home/public/application/components/tutorial_directory.test.tsx b/src/plugins/home/public/application/components/tutorial_directory.test.tsx
index eacd50fc43d0..6114f463304d 100644
--- a/src/plugins/home/public/application/components/tutorial_directory.test.tsx
+++ b/src/plugins/home/public/application/components/tutorial_directory.test.tsx
@@ -9,6 +9,8 @@ import { IntlProvider } from 'react-intl';
import { coreMock } from '../../../../../core/public/mocks';
import { setServices } from '../opensearch_dashboards_services';
import { getMockedServices } from '../opensearch_dashboards_services.mock';
+import * as utils from '../../../../../plugins/data_source_management/public/components/utils';
+import { DataSourceSelectionService } from '../../../../../plugins/data_source_management/public';
const makeProps = () => {
const coreMocks = coreMock.createStart();
@@ -28,6 +30,8 @@ describe('', () => {
it('should render home breadcrumbs when withoutHomeBreadCrumb is undefined', async () => {
const finalProps = makeProps();
currentService.http.get.mockResolvedValueOnce([]);
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(new DataSourceSelectionService());
+
// @ts-ignore
const { TutorialDirectory } = await import('./tutorial_directory');
render(
@@ -49,6 +53,8 @@ describe('', () => {
it('should not render home breadcrumbs when withoutHomeBreadCrumb is true', async () => {
const finalProps = makeProps();
currentService.http.get.mockResolvedValueOnce([]);
+ spyOn(utils, 'getDataSourceSelection').and.returnValue(new DataSourceSelectionService());
+
// @ts-ignore
const { TutorialDirectory } = await import('./tutorial_directory');
render(
diff --git a/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu.test.tsx.snap b/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu.test.tsx.snap
index 7a7063db0601..6397e6269129 100644
--- a/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu.test.tsx.snap
+++ b/src/plugins/navigation/public/top_nav_menu/__snapshots__/top_nav_menu.test.tsx.snap
@@ -45,6 +45,23 @@ exports[`TopNavMenu mounts the data source menu as well as top nav menu 1`] = `
}
}
componentType="DataSourceView"
+ dataSourceSelection={
+ DataSourceSelectionService {
+ "getSelection$": [Function],
+ "getSelectionValue": [Function],
+ "remove": [Function],
+ "selectDataSource": [Function],
+ "selectedDataSource$": BehaviorSubject {
+ "_isScalar": false,
+ "_value": Map {},
+ "closed": false,
+ "hasError": false,
+ "isStopped": false,
+ "observers": Array [],
+ "thrownError": null,
+ },
+ }
+ }
/>
@@ -71,6 +88,23 @@ exports[`TopNavMenu mounts the data source menu if showDataSourceMenu is true 1`
}
}
componentType="DataSourceView"
+ dataSourceSelection={
+ DataSourceSelectionService {
+ "getSelection$": [Function],
+ "getSelectionValue": [Function],
+ "remove": [Function],
+ "selectDataSource": [Function],
+ "selectedDataSource$": BehaviorSubject {
+ "_isScalar": false,
+ "_value": Map {},
+ "closed": false,
+ "hasError": false,
+ "isStopped": false,
+ "observers": Array [],
+ "thrownError": null,
+ },
+ }
+ }
/>
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
index a63aaf4d60d2..99644e49bde0 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
@@ -36,6 +36,7 @@ import { TopNavMenu } from './top_nav_menu';
import { TopNavMenuData } from './top_nav_menu_data';
import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import * as testUtils from '../../../data_source_management/public/components/utils';
+import { DataSourceSelectionService } from '../../../data_source_management/public/service/data_source_selection_service';
const dataShim = {
ui: {
@@ -63,6 +64,7 @@ describe('TopNavMenu', () => {
run: jest.fn(),
},
];
+ const dataSourceSelection = new DataSourceSelectionService();
it('Should render nothing when no config is provided', () => {
const component = shallowWithIntl();
@@ -122,6 +124,7 @@ describe('TopNavMenu', () => {
spyOn(testUtils, 'getApplication').and.returnValue({ id: 'test2' });
spyOn(testUtils, 'getUiSettings').and.returnValue({ id: 'test2' });
spyOn(testUtils, 'getHideLocalCluster').and.returnValue(true);
+ spyOn(testUtils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
const component = shallowWithIntl(
{
fullWidth: true,
activeOption: [{ label: 'what', id: '1' }],
},
+ dataSourceSelection,
}}
/>
);
@@ -144,6 +148,7 @@ describe('TopNavMenu', () => {
spyOn(testUtils, 'getApplication').and.returnValue({ id: 'test2' });
spyOn(testUtils, 'getUiSettings').and.returnValue({ id: 'test2' });
spyOn(testUtils, 'getHideLocalCluster').and.returnValue(true);
+ spyOn(testUtils, 'getDataSourceSelection').and.returnValue(dataSourceSelection);
const component = shallowWithIntl(
{
fullWidth: true,
activeOption: [{ label: 'what', id: '1' }],
},
+ dataSourceSelection: new DataSourceSelectionService(),
}}
/>
);