diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b3ecdcec69..344a3345b76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [MD] Add dropdown header to data source single selector ([#6431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6431)) - [Workspace] Add permission tab to workspace create update page ([#6378](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6378)) - [Workspace] Hide datasource and advanced settings menu in dashboard management when in workspace. ([#6455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6455)) +- [Multiple Datasource] Modify selectable picker to remove group label and close popover after selection ([#6515](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6515)) - [Workspace] Add workspaces filter to saved objects page. ([#6458](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6458)) ### 🐛 Bug Fixes diff --git a/examples/multiple_data_source_examples/public/application.tsx b/examples/multiple_data_source_examples/public/application.tsx index 16d9c928b04c..81248f8adccf 100644 --- a/examples/multiple_data_source_examples/public/application.tsx +++ b/examples/multiple_data_source_examples/public/application.tsx @@ -11,7 +11,7 @@ import { CoreStart, AppMountParameters } from '../../../src/core/public'; import { Home } from './components/home'; export const renderApp = ( - { notifications, http, savedObjects, application }: CoreStart, + { notifications, http, savedObjects, application, uiSettings }: CoreStart, dataSource: DataSourcePluginSetup, dataSourceManagement: DataSourceManagementPluginSetup, { appBasePath, element, setHeaderActionMenu }: AppMountParameters, @@ -28,6 +28,7 @@ export const renderApp = ( dataSourceManagement={dataSourceManagement} navigateToApp={application.navigateToApp} navigation={navigation} + uiSettings={uiSettings} />, element ); diff --git a/examples/multiple_data_source_examples/public/components/data_source_list_active_example.tsx b/examples/multiple_data_source_examples/public/components/data_source_list_active_example.tsx index 529d610c8132..81a15e2c03da 100644 --- a/examples/multiple_data_source_examples/public/components/data_source_list_active_example.tsx +++ b/examples/multiple_data_source_examples/public/components/data_source_list_active_example.tsx @@ -28,6 +28,7 @@ interface DataSourceListActiveExampleProps { notifications: CoreStart['notifications']; setActionMenu?: (menuMount: MountPoint | undefined) => void; dataSourceManagement: DataSourceManagementPluginSetup; + uiSettings: CoreStart['uiSettings']; } export const DataSourceListActiveExample = ({ @@ -36,6 +37,7 @@ export const DataSourceListActiveExample = ({ notifications, setActionMenu, dataSourceManagement, + uiSettings, }: DataSourceListActiveExampleProps) => { const DataSourceMenu = dataSourceManagement.ui.getDataSourceMenu< DataSourceAggregatedViewConfig @@ -106,7 +108,10 @@ export const DataSourceListActiveExample = ({ fullWidth: false, savedObjects: savedObjects.client, notifications, - displayAllCompatibleDataSources: true, + displayAllCompatibleDataSources: false, + // To see selected options, obtain the datasource id and paste it here. Note that this needs to be defined when displayAllCompatibleDataSources is false + activeDataSourceIds: [], + uiSettings, }} /> )} diff --git a/examples/multiple_data_source_examples/public/components/data_source_list_all_example.tsx b/examples/multiple_data_source_examples/public/components/data_source_list_all_example.tsx index 26545fef70c7..570e51be36e9 100644 --- a/examples/multiple_data_source_examples/public/components/data_source_list_all_example.tsx +++ b/examples/multiple_data_source_examples/public/components/data_source_list_all_example.tsx @@ -28,6 +28,7 @@ interface DataSourceListAllExampleProps { notifications: CoreStart['notifications']; setActionMenu?: (menuMount: MountPoint | undefined) => void; dataSourceManagement: DataSourceManagementPluginSetup; + uiSettings: CoreStart['uiSettings']; } export const DataSourceListAllExample = ({ @@ -36,6 +37,7 @@ export const DataSourceListAllExample = ({ notifications, setActionMenu, dataSourceManagement, + uiSettings, }: DataSourceListAllExampleProps) => { const DataSourceMenu = dataSourceManagement.ui.getDataSourceMenu< DataSourceAggregatedViewConfig @@ -100,6 +102,7 @@ export const DataSourceListAllExample = ({ savedObjects: savedObjects.client, notifications, displayAllCompatibleDataSources: true, + uiSettings, }} /> )} diff --git a/examples/multiple_data_source_examples/public/components/home.tsx b/examples/multiple_data_source_examples/public/components/home.tsx index fe6ca1f285a4..c7a61c590c63 100644 --- a/examples/multiple_data_source_examples/public/components/home.tsx +++ b/examples/multiple_data_source_examples/public/components/home.tsx @@ -22,6 +22,7 @@ export interface HomeProps { notifications: CoreStart['notifications']; http: CoreStart['http']; savedObjects: CoreStart['savedObjects']; + uiSettings: CoreStart['uiSettings']; dataSourceEnabled: boolean; dataSourceManagement: DataSourceManagementPluginSetup; navigateToApp: CoreStart['application']['navigateToApp']; @@ -65,6 +66,7 @@ export const Home = ({ basename, notifications, savedObjects, + uiSettings, dataSourceEnabled, setActionMenu, dataSourceManagement, @@ -133,6 +135,7 @@ export const Home = ({ dataSourceEnabled={dataSourceEnabled} setActionMenu={setActionMenu} dataSourceManagement={dataSourceManagement} + uiSettings={uiSettings} /> ), }, @@ -146,6 +149,7 @@ export const Home = ({ dataSourceEnabled={dataSourceEnabled} setActionMenu={setActionMenu} dataSourceManagement={dataSourceManagement} + uiSettings={uiSettings} /> ), }, 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 e4f4b1f67c67..57ba243a476b 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 @@ -1,119 +1,155 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataSourceAggregatedView should render normally with data source filter 1`] = ` - + } - } -> - - - - - - All - - + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false) should render normally with local cluster and active selections configured 2`] = ` + } closePopover={[Function]} @@ -124,169 +160,687 @@ exports[`DataSourceAggregatedView should render normally with data source filter ownFocus={true} panelPaddingSize="none" > -
-
- + + + + - - -
-
+ + + + + + + +
-
+ `; -exports[`DataSourceAggregatedView should render normally with local cluster and actice selections 1`] = ` - + } - } - savedObjectsClient={ - Object { - "find": [MockFunction] { - "calls": Array [ - Array [ + 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 4`] = ` + + } - } -> - - - - + + + + + + + + + +`; + +exports[`DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false) should render normally with local cluster and active selections configured 6`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" > - - 1 - - + + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false) should render normally with local cluster and active selections configured 7`] = ` + } closePopover={[Function]} @@ -297,165 +851,564 @@ exports[`DataSourceAggregatedView should render normally with local cluster and ownFocus={true} panelPaddingSize="none" > -
-
- + + + + - - -
-
+ + + + + + + +
-
+ `; -exports[`DataSourceAggregatedView should render normally with local cluster hidden and all options 1`] = ` - + } - } - savedObjectsClient={ - Object { - "find": [MockFunction] { - "calls": Array [ - Array [ + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 1`] = ` + + } - } -> - - - - + + + + + + +`; + +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 3`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" > - - 0 - - + + + + + + + + + + + + + +`; + +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 4`] = ` + } closePopover={[Function]} @@ -466,78 +1419,96 @@ exports[`DataSourceAggregatedView should render normally with local cluster hidd ownFocus={true} panelPaddingSize="none" > -
-
- + + + + - - -
-
+ + + + +
-
+ `; -exports[`DataSourceAggregatedView should render normally with local cluster not hidden and all options 1`] = ` +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 5`] = ` - - Data sources - - - All - } closePopover={[Function]} @@ -548,246 +1519,579 @@ exports[`DataSourceAggregatedView should render normally with local cluster not ownFocus={true} panelPaddingSize="none" > - + + + + + + + + + + + + `; -exports[`DataSourceAggregatedView should render popup when clicking on info icon 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
- - - 1 - -
-
+ + + + + + +`; + +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 7`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + - -
-
-
-
-
- -
- , - "container":
- - - 1 - -
+ + + + + + + +`; + +exports[`DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true) should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out 10`] = ` + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceSViewContextMenuPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + -
- -
-
-
, - "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_aggregated_view/data_source_aggregated_view.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss new file mode 100644 index 000000000000..9e8d78eac91b --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss @@ -0,0 +1,19 @@ +.dataSourceAggregatedViewOuiPanel { + width: 300px; + + .dataSourceAggregatedViewOuiFlexGroup { + .dataSourceAggregatedViewOuiFlexItem { + color: grey; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + } + } + + .dataSourceAggregatedViewOuiSwitch { + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx index 5bb518af2d06..5251ed199cca 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.test.tsx @@ -3,124 +3,269 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ShallowWrapper, mount, shallow } from 'enzyme'; +import { ShallowWrapper, shallow } from 'enzyme'; import React from 'react'; import { DataSourceAggregatedView } from './data_source_aggregated_view'; -import { SavedObjectsClientContract } from '../../../../../core/public'; -import { notificationServiceMock } from '../../../../../core/public/mocks'; -import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks'; -import { render } from '@testing-library/react'; +import { SavedObject, SavedObjectsClientContract } from '../../../../../core/public'; +import { + applicationServiceMock, + notificationServiceMock, + uiSettingsServiceMock, +} from '../../../../../core/public/mocks'; +import { + getDataSourcesWithFieldsResponse, + mockResponseForSavedObjectsCalls, + mockUiSettingsCalls, +} from '../../mocks'; +import * as utils from '../utils'; +import { EuiSelectable, EuiSwitch } from '@elastic/eui'; +import { DataSourceAttributes } from '../../types'; -describe('DataSourceAggregatedView', () => { +describe('DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true)', () => { let component: ShallowWrapper, React.Component<{}, {}, any>>; let client: SavedObjectsClientContract; const { toasts } = notificationServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const application = applicationServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); beforeEach(() => { client = { find: jest.fn().mockResolvedValue([]), } as any; mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse); + mockUiSettingsCalls(uiSettings, 'get', 'test1'); + jest.spyOn(utils, 'getApplication').mockReturnValue(application); }); - it('should render normally with local cluster not hidden and all options', () => { - 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.each([ + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + activeDataSourceIds: undefined, + hideLocalCluster: false, + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + activeDataSourceIds: ['non-existent-id'], + hideLocalCluster: false, + }, + { + filter: undefined, + activeDataSourceIds: undefined, + hideLocalCluster: false, + }, + { + filter: undefined, + activeDataSourceIds: ['non-existent-id'], + hideLocalCluster: false, + }, + { + filter: undefined, + activeDataSourceIds: ['test1'], + hideLocalCluster: false, + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + activeDataSourceIds: undefined, + hideLocalCluster: true, + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + activeDataSourceIds: ['non-existent-id'], + hideLocalCluster: true, + }, + { + filter: undefined, + activeDataSourceIds: undefined, + hideLocalCluster: true, + }, + { + filter: undefined, + activeDataSourceIds: ['non-existent-id'], + hideLocalCluster: true, + }, + { + filter: undefined, + activeDataSourceIds: ['test1'], + hideLocalCluster: true, + }, + ])( + 'should render normally with local cluster configured, default datasource removed or added, and if activeDataSourceIds is present or filtered out', + async ({ filter, activeDataSourceIds, hideLocalCluster }) => { + component = shallow( + + ); - it('should render normally with local cluster hidden and all options', () => { - const container = mount( - - ); - expect(container).toMatchSnapshot(); - expect(client.find).toBeCalledWith({ - fields: ['id', 'title', 'auth.type'], - perPage: 10000, - type: 'data-source', - }); - expect(toasts.addWarning).toBeCalledTimes(0); - const badge = container.find('EuiNotificationBadge').text(); - expect(badge).toEqual('0'); - }); + // Renders normally + expect(component).toMatchSnapshot(); + expect(client.find).toBeCalledWith({ + fields: ['id', 'title', 'auth.type'], + perPage: 10000, + type: 'data-source', + }); + expect(toasts.addWarning).toBeCalledTimes(0); + await nextTick(); - it('should render normally with local cluster and actice selections', () => { - const container = mount( - - ); - expect(container).toMatchSnapshot(); - expect(client.find).toBeCalledWith({ - fields: ['id', 'title', 'auth.type'], - perPage: 10000, - type: 'data-source', - }); - expect(toasts.addWarning).toBeCalledTimes(0); - const badge = container.find('EuiNotificationBadge').text(); - expect(badge).toEqual('1'); - }); + // Renders correctly for hide local cluster configuration + if (!hideLocalCluster) { + expect(component.find(EuiSelectable).prop('options')).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '' })]) + ); + } else { + expect(component.find(EuiSelectable).prop('options')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: '' })]) + ); + } - it('should render normally with data source filter', () => { - const container = mount( - ds.attributes.auth.type !== 'no_auth'} - /> - ); - expect(container).toMatchSnapshot(); - expect(client.find).toBeCalledWith({ - fields: ['id', 'title', 'auth.type'], - perPage: 10000, - type: 'data-source', - }); - expect(toasts.addWarning).toBeCalledTimes(0); - const badge = container.find('EuiNotificationBadge').text(); - expect(badge).toEqual('All'); - }); + // Renders correctly when default datasource is filtered out or not + if (!filter) { + expect(component.find(EuiSelectable).prop('options')).toEqual( + expect.arrayContaining([expect.objectContaining({ id: 'test1' })]) + ); + } else { + expect(component.find(EuiSelectable).prop('options')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: 'test1' })]) + ); + } + + // All renders should not have a switch + expect(component.find(EuiSwitch).exists()).toBeFalsy(); + } + ); +}); + +describe('DataSourceAggregatedView: read active view (displayAllCompatibleDataSources is set to false)', () => { + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + const uiSettings = uiSettingsServiceMock.createStartContract(); + const nextTick = () => new Promise((res) => process.nextTick(res)); - it('should render popup when clicking on info icon', async () => { - const container = render( - - ); - const infoIcon = await container.findByTestId('dataSourceAggregatedViewInfoButton'); - infoIcon.click(); - expect(container).toMatchSnapshot(); + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse); + mockUiSettingsCalls(uiSettings, 'get', 'test1'); }); + + it.each([ + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + hideLocalCluster: false, + activeDataSourceIds: ['test1', 'test2'], + }, + { + filter: undefined, + hideLocalCluster: false, + activeDataSourceIds: ['test1', 'test2'], + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + hideLocalCluster: true, + activeDataSourceIds: ['test1', 'test2'], + }, + { + filter: undefined, + hideLocalCluster: true, + activeDataSourceIds: ['test1', 'test2'], + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + hideLocalCluster: false, + activeDataSourceIds: [], + }, + { + filter: undefined, + hideLocalCluster: false, + activeDataSourceIds: [], + }, + { + filter: (ds: SavedObject) => { + return ds.id !== 'test1'; + }, + hideLocalCluster: true, + activeDataSourceIds: [], + }, + { + filter: undefined, + hideLocalCluster: true, + activeDataSourceIds: [], + }, + ])( + 'should render normally with local cluster and active selections configured', + async ({ filter, hideLocalCluster, activeDataSourceIds }) => { + const component = shallow( + + ); + await nextTick(); + + // Should render normally + expect(component).toMatchSnapshot(); + expect(client.find).toBeCalledWith({ + fields: ['id', 'title', 'auth.type'], + perPage: 10000, + type: 'data-source', + }); + expect(toasts.addWarning).toBeCalledTimes(0); + + // Should render only active options + const euiSwitch = component.find(EuiSwitch); + expect(euiSwitch.exists()).toBeTruthy(); + euiSwitch.prop('onChange')({ target: { checked: true } }); + const expectedOptions = activeDataSourceIds.length + ? [ + { + id: 'test2', + label: 'test2', + disabled: true, + checked: 'on', + }, + ] + : []; + + if (!filter && activeDataSourceIds.length) { + expectedOptions.push({ + id: 'test1', + label: 'test1', + disabled: true, + checked: 'on', + }); + } + expect(component.find(EuiSelectable).prop('options')).toEqual( + expect.arrayContaining(expectedOptions) + ); + } + ); }); 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 c77a49a75831..16b63d36d912 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 @@ -6,19 +6,28 @@ import React from 'react'; import { EuiButtonEmpty, - EuiButtonIcon, - EuiContextMenu, - EuiNotificationBadge, + EuiContextMenuPanel, + EuiPanel, EuiPopover, + EuiSelectable, + EuiSwitch, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; -import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + ToastsStart, +} from 'opensearch-dashboards/public'; +import { getApplication, getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; import { NoDataSource } from '../no_data_source'; import { DataSourceErrorMenu } from '../data_source_error_menu'; import { DataSourceBaseState } from '../data_source_menu/types'; +import { DataSourceOption } from '../data_source_menu/types'; +import { DataSourceItem } from '../data_source_item'; +import { DataSourceDropDownHeader } from '../drop_down_header'; +import './data_source_aggregated_view.scss'; interface DataSourceAggregatedViewProps { savedObjectsClient: SavedObjectsClientContract; @@ -28,11 +37,19 @@ interface DataSourceAggregatedViewProps { activeDataSourceIds?: string[]; dataSourceFilter?: (dataSource: SavedObject) => boolean; displayAllCompatibleDataSources: boolean; + uiSettings?: IUiSettingsClient; } interface DataSourceAggregatedViewState extends DataSourceBaseState { isPopoverOpen: boolean; allDataSourcesIdToTitleMap: Map; + switchChecked: boolean; + defaultDataSource: string | null; +} + +interface DataSourceOptionDisplay extends DataSourceOption { + disabled?: boolean; + checked?: string; } export class DataSourceAggregatedView extends React.Component< @@ -49,6 +66,8 @@ export class DataSourceAggregatedView extends React.Component< allDataSourcesIdToTitleMap: new Map(), showEmptyState: false, showError: false, + switchChecked: false, + defaultDataSource: null, }; } @@ -56,10 +75,14 @@ export class DataSourceAggregatedView extends React.Component< this._isMounted = false; } - onClick() { + onDataSourcesClick() { this.setState({ ...this.state, isPopoverOpen: !this.state.isPopoverOpen }); } + onSwitchClick(e) { + this.setState({ ...this.state, switchChecked: e.target.checked }); + } + closePopover() { this.setState({ ...this.state, isPopoverOpen: false }); } @@ -68,6 +91,8 @@ export class DataSourceAggregatedView extends React.Component< this._isMounted = true; getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type']) .then((fetchedDataSources) => { + const allDataSourcesIdToTitleMap = new Map(); + if (fetchedDataSources?.length) { let filteredDataSources = fetchedDataSources; if (this.props.dataSourceFilter) { @@ -75,22 +100,24 @@ export class DataSourceAggregatedView extends React.Component< this.props.dataSourceFilter!(ds) ); } - const allDataSourcesIdToTitleMap = new Map(); filteredDataSources.forEach((ds) => { allDataSourcesIdToTitleMap.set(ds.id, ds.attributes!.title || ''); }); - if (!this.props.hideLocalCluster) { - allDataSourcesIdToTitleMap.set('', 'Local cluster'); - } - if (!this._isMounted) return; - this.setState({ - ...this.state, - allDataSourcesIdToTitleMap, - }); } + + if (!this.props.hideLocalCluster) { + allDataSourcesIdToTitleMap.set('', 'Local cluster'); + } + + this.setState({ + ...this.state, + allDataSourcesIdToTitleMap, + defaultDataSource: this.props.uiSettings?.get('defaultDataSource', null) ?? null, + showEmptyState: allDataSourcesIdToTitleMap.size === 0, + }); }) .catch(() => { handleDataSourceFetchError(this.onError.bind(this), this.props.notifications); @@ -109,65 +136,64 @@ export class DataSourceAggregatedView extends React.Component< return ; } const button = ( - ); - let items = []; + let items: DataSourceOptionDisplay[] = []; // only display active data sources - if (this.props.displayAllCompatibleDataSources) { - items = [...this.state.allDataSourcesIdToTitleMap.values()].map((title) => { - return { - name: title, - disabled: true, - }; - }); + if (!this.props.displayAllCompatibleDataSources && this.state.switchChecked) { + items = this.props + .activeDataSourceIds!.filter((id) => this.state.allDataSourcesIdToTitleMap.has(id)) + .map((id) => { + return { + id, + label: this.state.allDataSourcesIdToTitleMap.get(id), + disabled: true, + checked: 'on', + }; + }); } else { - items = this.props.activeDataSourceIds!.map((id) => { - return { - name: this.state.allDataSourcesIdToTitleMap.get(id), + this.state.allDataSourcesIdToTitleMap.forEach((label, id) => { + items.push({ + id, + label, disabled: true, - }; + checked: + !this.props.displayAllCompatibleDataSources && + this.props.activeDataSourceIds && + this.props.activeDataSourceIds.length && + this.props.activeDataSourceIds.includes(id) + ? 'on' + : undefined, + }); }); } - const title = this.props.displayAllCompatibleDataSources - ? `Data sources (${this.state.allDataSourcesIdToTitleMap.size})` - : 'Selected data sources'; + const numSelectedItems = items.filter((item) => item.checked === 'on').length; - const panels = [ - { - id: 0, - title, - items, - }, - ]; + const titleComponent = ( + + ); return ( <> - - {'Data sources'} - - - {(this.props.displayAllCompatibleDataSources && 'All') || - this.props.activeDataSourceIds!.length} - - + + + {titleComponent} + + + + ( + + )} + > + {(list) => list} + + + + {!this.props.displayAllCompatibleDataSources && ( + + this.onSwitchClick(e)} + compressed={true} + /> + + )} + ); 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 517f8de733ba..3ec264b888ff 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 @@ -68,33 +68,6 @@ Object { "asFragment": [Function], "baseElement":
- - - All -
, "container":
- - - All -
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 4353dfc0bbf7..c8325a5b14b4 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 @@ -5,17 +5,19 @@ import { ShallowWrapper, shallow } from 'enzyme'; import { SavedObjectsClientContract } from '../../../../../core/public'; -import { notificationServiceMock } from '../../../../../core/public/mocks'; +import { applicationServiceMock, notificationServiceMock } from '../../../../../core/public/mocks'; import React from 'react'; import { DataSourceMenu } from './data_source_menu'; import { render } from '@testing-library/react'; import { DataSourceComponentType } from './types'; +import * as utils from '../utils'; describe('DataSourceMenu', () => { let component: ShallowWrapper, React.Component<{}, {}, any>>; let client: SavedObjectsClientContract; const notifications = notificationServiceMock.createStartContract(); + const application = applicationServiceMock.createStartContract(); beforeEach(() => { client = { @@ -112,6 +114,7 @@ describe('DataSourceMenu', () => { }); it('should render data source aggregated view', () => { + jest.spyOn(utils, 'getApplication').mockReturnValue(application); const container = render( (props: DataSourceMenuProps): ReactElement | notifications={notifications!.toasts} activeDataSourceIds={activeDataSourceIds} dataSourceFilter={dataSourceFilter} - displayAllCompatibleDataSources={displayAllCompatibleDataSources || false} + displayAllCompatibleDataSources={displayAllCompatibleDataSources} + uiSettings={uiSettings} /> ); } 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 d6d960c0e40a..3708291f0a89 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 @@ -59,14 +59,23 @@ export interface DataSourceViewConfig extends DataSourceBaseConfig { onSelectedDataSources?: (dataSources: DataSourceOption[]) => void; } -export interface DataSourceAggregatedViewConfig extends DataSourceBaseConfig { +interface DataSourceAggregatedViewBaseConfig extends DataSourceBaseConfig { savedObjects: SavedObjectsClientContract; notifications: NotificationsStart; - activeDataSourceIds?: string[]; - displayAllCompatibleDataSources?: boolean; dataSourceFilter?: (dataSource: SavedObject) => boolean; + uiSettings?: IUiSettingsClient; } +export type DataSourceAggregatedViewConfig = + | (DataSourceAggregatedViewBaseConfig & { + activeDataSourceIds: string[]; + displayAllCompatibleDataSources: false; + }) + | (DataSourceAggregatedViewBaseConfig & { + activeDataSourceIds?: string[]; + displayAllCompatibleDataSources: true; + }); + export interface DataSourceSelectableConfig extends DataSourceBaseConfig { onSelectedDataSources: (dataSources: DataSourceOption[]) => void; savedObjects: SavedObjectsClientContract; diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap index 8089f79d5487..79d158ee3be5 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap @@ -54,11 +54,6 @@ exports[`DataSourceSelectable should filter options if configured 1`] = ` onChange={[Function]} options={ Array [ - Object { - "id": "opensearchClusterGroupLabel", - "isGroupLabel": true, - "label": "OpenSearch cluster", - }, Object { "checked": "on", "id": "", 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 1ced7e3872c1..37f84bb9c142 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 @@ -403,89 +403,4 @@ describe('DataSourceSelectable', () => { expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); expect(onSelectedDataSource).toHaveBeenCalled(); }); - - it('should render opensearch cluster group label at the top of options, when there are options availiable', async () => { - const onSelectedDataSource = jest.fn(); - component = shallow( - - ); - - component.instance().componentDidMount!(); - await nextTick(); - const optionsProp = component.find(EuiSelectable).prop('options'); - expect(optionsProp[0]).toEqual(dataSourceOptionGroupLabel.opensearchCluster); - }); - - it('should not render opensearch cluster group label, when there is no option availiable', async () => { - const onSelectedDataSource = jest.fn(); - spyOn(utils, 'getDefaultDataSource').and.returnValue([]); - component = shallow( - - ); - - component.instance().componentDidMount!(); - await nextTick(); - const optionsProp = component.find(EuiSelectable).prop('options'); - expect(optionsProp).toEqual([]); - }); - - it('should render group lablel normally after onChange', async () => { - const onSelectedDataSource = jest.fn(); - component = shallow( - - ); - const componentInstance = component.instance(); - - componentInstance.componentDidMount!(); - await nextTick(); - const optionsPropBefore = component.find(EuiSelectable).prop('options'); - expect(optionsPropBefore).toEqual([ - dataSourceOptionGroupLabel.opensearchCluster, - { - id: 'test1', - label: 'test1', - checked: 'on', - }, - { - id: 'test2', - label: 'test2', - }, - { - id: 'test3', - label: 'test3', - }, - ]); - componentInstance.onChange([ - dataSourceOptionGroupLabel.opensearchCluster, - { id: 'test2', label: 'test2', checked: 'on' }, - ]); - await nextTick(); - const optionsPropAfter = component.find(EuiSelectable).prop('options'); - expect(optionsPropAfter).toEqual([ - dataSourceOptionGroupLabel.opensearchCluster, - { id: 'test2', label: 'test2', checked: 'on' }, - ]); - }); }); 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 ec4af533c5df..6586e7bbdcd5 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 @@ -21,7 +21,6 @@ import { ToastsStart, } from 'opensearch-dashboards/public'; import { - dataSourceOptionGroupLabel, getDataSourcesWithFields, getDefaultDataSource, getFilteredDataSources, @@ -30,11 +29,7 @@ import { import { LocalCluster } from '../data_source_selector/data_source_selector'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; -import { - DataSourceBaseState, - DataSourceGroupLabelOption, - DataSourceOption, -} from '../data_source_menu/types'; +import { DataSourceBaseState, DataSourceOption } from '../data_source_menu/types'; import { DataSourceErrorMenu } from '../data_source_error_menu'; import { DataSourceItem } from '../data_source_item'; import { NoDataSource } from '../no_data_source'; @@ -63,12 +58,6 @@ interface DataSourceSelectableState extends DataSourceBaseState { defaultDataSource: string | null; } -export const opensearchClusterGroupLabel: DataSourceGroupLabelOption = { - id: 'opensearchClusterGroupLabel', - label: 'OpenSearch cluster', - isGroupLabel: true, -}; - export class DataSourceSelectable extends React.Component< DataSourceSelectableProps, DataSourceSelectableState @@ -221,16 +210,14 @@ export class DataSourceSelectable extends React.Component< onChange(options: DataSourceOption[]) { if (!this._isMounted) return; - const optionsWithoutGroupLabel = options.filter( - (option) => !option.hasOwnProperty('isGroupLabel') - ); const selectedDataSource = options.find(({ checked }) => checked); - this.setState({ dataSourceOptions: optionsWithoutGroupLabel }); + this.setState({ dataSourceOptions: options }); if (selectedDataSource) { this.setState({ selectedOption: [selectedDataSource], + isPopoverOpen: false, }); this.props.onSelectedDataSources([ @@ -239,16 +226,6 @@ export class DataSourceSelectable extends React.Component< } } - getOptionsWithGroupLabel = (dataSourceOptions: DataSourceOption[]): DataSourceOption[] => { - let optionsWithGroupLabel: DataSourceOption[] = []; - if (dataSourceOptions.length === 0) { - optionsWithGroupLabel = []; - } else { - optionsWithGroupLabel = [dataSourceOptionGroupLabel.opensearchCluster, ...dataSourceOptions]; - } - return optionsWithGroupLabel; - }; - render() { if (this.state.showEmptyState) { return ; @@ -303,7 +280,7 @@ export class DataSourceSelectable extends React.Component< placeholder: 'Search', compressed: true, }} - options={this.getOptionsWithGroupLabel(this.state.dataSourceOptions)} + options={this.state.dataSourceOptions} onChange={(newOptions) => this.onChange(newOptions)} singleSelection={true} data-test-subj={'dataSourceSelectable'}