From dbf9df1db7ce606bb8c0897194480f43848d1434 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:16:11 -0700 Subject: [PATCH] [Multiple Datasource] Add empty icon and fix empty state handling (#6597) (#6618) * add empty icon and fix empty state handling * add back missing import --------- (cherry picked from commit 543bbeadb4cac9fda568b626d5c24223a78f3291) Signed-off-by: Lu Yu Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: ZilongX <99905560+ZilongX@users.noreply.github.com> --- .../custom_database_icon/empty_icon.tsx | 36 ++++ .../components/custom_database_icon/index.ts | 1 + .../data_source_aggregated_view.tsx | 22 ++- .../data_source_multi_selectable.tsx | 23 ++- .../data_source_selectable.test.tsx | 6 +- .../data_source_selectable.tsx | 22 ++- .../data_source_view.test.tsx.snap | 110 +++++++++++- .../data_source_view.test.tsx | 11 +- .../data_source_view/data_source_view.tsx | 19 +- .../no_data_source.test.tsx.snap | 168 ++++++++---------- .../no_data_source/no_data_source.test.tsx | 4 +- .../no_data_source/no_data_source.tsx | 76 +++----- .../public/components/toast_button/index.ts | 1 + .../manage_data_source_button.tsx | 32 ++++ .../public/components/utils.test.ts | 5 +- .../public/components/utils.ts | 23 ++- 16 files changed, 365 insertions(+), 194 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.tsx create mode 100644 src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.tsx b/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.tsx new file mode 100644 index 000000000000..6147b8064c3b --- /dev/null +++ b/src/plugins/data_source_management/public/components/custom_database_icon/empty_icon.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; + +export const EmptyIcon = () => { + return ( + + + + + + + + + ); +}; diff --git a/src/plugins/data_source_management/public/components/custom_database_icon/index.ts b/src/plugins/data_source_management/public/components/custom_database_icon/index.ts index 506c9ee84980..262713b0b9fa 100644 --- a/src/plugins/data_source_management/public/components/custom_database_icon/index.ts +++ b/src/plugins/data_source_management/public/components/custom_database_icon/index.ts @@ -3,3 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ export { ErrorIcon } from './error_icon'; +export { EmptyIcon } from './empty_icon'; 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 4aa8a31e848e..b9433e5ca189 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 @@ -19,7 +19,12 @@ import { SavedObjectsClientContract, ToastsStart, } from 'opensearch-dashboards/public'; -import { getApplication, getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; +import { + getApplication, + getDataSourcesWithFields, + handleDataSourceFetchError, + handleNoAvailableDataSourceError, +} from '../utils'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; import { NoDataSource } from '../no_data_source'; @@ -114,6 +119,15 @@ export class DataSourceAggregatedView extends React.Component< allDataSourcesIdToTitleMap.set('', 'Local cluster'); } + if (allDataSourcesIdToTitleMap.size === 0) { + handleNoAvailableDataSourceError( + this.onEmptyState.bind(this), + this.props.notifications, + this.props.application + ); + return; + } + this.setState({ ...this.state, allDataSourcesIdToTitleMap, @@ -126,13 +140,17 @@ export class DataSourceAggregatedView extends React.Component< }); } + onEmptyState() { + this.setState({ showEmptyState: true }); + } + onError() { this.setState({ showError: true }); } render() { if (this.state.showEmptyState) { - return ; + return ; } if (this.state.showError) { return ; 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 ddc9477061ce..85506ec84b61 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 @@ -12,7 +12,11 @@ import { import { IUiSettingsClient } from 'src/core/public'; import { DataSourceFilterGroup, SelectedDataSourceOption } from './data_source_filter_group'; import { NoDataSource } from '../no_data_source'; -import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; +import { + getDataSourcesWithFields, + handleDataSourceFetchError, + handleNoAvailableDataSourceError, +} from '../utils'; import { DataSourceBaseState } from '../data_source_menu/types'; import { DataSourceErrorMenu } from '../data_source_error_menu'; @@ -85,11 +89,20 @@ export class DataSourceMultiSelectable extends React.Component< if (!this._isMounted) return; + if (selectedOptions.length === 0) { + handleNoAvailableDataSourceError( + this.onEmptyState.bind(this), + this.props.notifications, + this.props.application, + this.props.onSelectedDataSources + ); + return; + } + this.setState({ ...this.state, selectedOptions, defaultDataSource, - showEmptyState: (fetchedDataSources?.length === 0 && this.props.hideLocalCluster) || false, }); this.props.onSelectedDataSources(selectedOptions); @@ -102,6 +115,10 @@ export class DataSourceMultiSelectable extends React.Component< } } + onEmptyState() { + this.setState({ showEmptyState: true }); + } + onError() { this.setState({ showError: true }); } @@ -116,7 +133,7 @@ export class DataSourceMultiSelectable extends React.Component< render() { if (this.state.showEmptyState) { - return ; + return ; } if (this.state.showError) { return ; 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 63a67964a6ad..bb63d818df1d 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 @@ -405,7 +405,7 @@ describe('DataSourceSelectable', () => { }); it('should render no data source when no data source filtered out and hide local cluster', async () => { const onSelectedDataSource = jest.fn(); - const container = render( + render( { /> ); await nextTick(); - const button = await container.findByTestId('dataSourceEmptyStateHeaderButton'); - expect(button).toHaveTextContent('No data sources'); + expect(toasts.add).toBeCalled(); + expect(onSelectedDataSource).toBeCalledWith([]); }); }); 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 082e7ffa9669..7fa02be4dd15 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,7 @@ import { getDefaultDataSource, getFilteredDataSources, handleDataSourceFetchError, + handleNoAvailableDataSourceError, } from '../utils'; import { LocalCluster } from '../data_source_selector/data_source_selector'; import { SavedObject } from '../../../../../core/public'; @@ -185,6 +186,16 @@ export class DataSourceSelectable extends React.Component< dataSourceOptions.unshift(LocalCluster); } + if (dataSourceOptions.length === 0) { + handleNoAvailableDataSourceError( + this.onEmptyState.bind(this), + this.props.notifications, + this.props.application, + this.props.onSelectedDataSources + ); + return; + } + const defaultDataSource = this.props.uiSettings?.get('defaultDataSource', null) ?? null; if (this.props.selectedOption?.length) { @@ -203,6 +214,10 @@ export class DataSourceSelectable extends React.Component< } } + onEmptyState() { + this.setState({ showEmptyState: true }); + } + onError() { this.setState({ showError: true }); } @@ -227,12 +242,7 @@ export class DataSourceSelectable extends React.Component< render() { if (this.state.showEmptyState) { - return ( - - ); + return ; } if (this.state.showError) { diff --git a/src/plugins/data_source_management/public/components/data_source_view/__snapshots__/data_source_view.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_view/__snapshots__/data_source_view.test.tsx.snap index 80667bf86091..48037c688441 100644 --- a/src/plugins/data_source_management/public/components/data_source_view/__snapshots__/data_source_view.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_view/__snapshots__/data_source_view.test.tsx.snap @@ -62,15 +62,113 @@ exports[`DataSourceView Should render successfully when provided datasource has `; exports[`DataSourceView Should return error when provided datasource has been filtered out 1`] = ` - + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceViewPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + + + + + + + + + `; exports[`DataSourceView When selected option is local cluster and hide local Cluster is true, should return error 1`] = ` - + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="dataSourceViewPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + + + + + + + + + `; exports[`DataSourceView should call getDataSourceById when only pass id with no label 1`] = ` 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 ccfb70a8f2fd..07c36e414141 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 @@ -39,17 +39,18 @@ describe('DataSourceView', () => { expect(toasts.addWarning).toBeCalledTimes(0); }); it('When selected option is local cluster and hide local Cluster is true, should return error', () => { + const onSelectedDataSources = jest.fn(); component = shallow( ); expect(component).toMatchSnapshot(); - expect(toasts.addWarning).toBeCalledTimes(1); + expect(onSelectedDataSources).toBeCalledWith([]); }); it('Should return error when provided datasource has been filtered out', async () => { component = shallow( @@ -65,7 +66,6 @@ describe('DataSourceView', () => { /> ); expect(component).toMatchSnapshot(); - expect(toasts.addWarning).toBeCalledTimes(1); }); it('Should render successfully when provided datasource has not been filtered out', async () => { spyOn(utils, 'getDataSourceById').and.returnValue([{ id: 'test1', label: 'test1' }]); @@ -139,7 +139,7 @@ describe('DataSourceView', () => { it('should render no data source when no data source filtered out and hide local cluster', async () => { const onSelectedDataSource = jest.fn(); - const container = render( + render( { dataSourceFilter={(ds) => false} /> ); - const button = await container.findByTestId('dataSourceEmptyStateHeaderButton'); - expect(button).toHaveTextContent('No data sources'); + expect(onSelectedDataSource).toBeCalledWith([]); }); }); 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 1c7e93b5929c..eee5608ee27b 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 @@ -20,15 +20,10 @@ 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, - handleNoAvailableDataSourceError, -} from '../utils'; +import { getDataSourceById, handleDataSourceFetchError } from '../utils'; import { DataSourceDropDownHeader } from '../drop_down_header'; import { DataSourceItem } from '../data_source_item'; import { LocalCluster } from '../constants'; -import { NoDataSource } from '../no_data_source'; import './data_source_view.scss'; interface DataSourceViewProps { @@ -58,7 +53,7 @@ export class DataSourceView extends React.Component - ); - } if (this.state.showError) { return ; } diff --git a/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap index 6807943e0897..ee8f2120012f 100644 --- a/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap @@ -4,24 +4,20 @@ exports[`NoDataSource should render correctly with the provided totalDataSourceC - No data sources - + /> } closePopover={[Function]} data-test-subj="dataSourceEmptyStatePopover" display="inlineBlock" hasArrow={true} id="dataSourceEmptyStatePopover" - initialFocus=".euiSelectableSearch" isOpen={false} ownFocus={true} panelPaddingSize="none" @@ -30,50 +26,48 @@ exports[`NoDataSource should render correctly with the provided totalDataSourceC totalDataSourceCount={0} /> - - - - - - - - - - + + + + + + + + - + @@ -85,7 +79,7 @@ exports[`NoDataSource should render correctly with the provided totalDataSourceC - + `; @@ -93,24 +87,20 @@ exports[`NoDataSource should render normally 1`] = ` - No data sources - + /> } closePopover={[Function]} data-test-subj="dataSourceEmptyStatePopover" display="inlineBlock" hasArrow={true} id="dataSourceEmptyStatePopover" - initialFocus=".euiSelectableSearch" isOpen={false} ownFocus={true} panelPaddingSize="none" @@ -119,50 +109,48 @@ exports[`NoDataSource should render normally 1`] = ` totalDataSourceCount={0} /> - - - - - - - - - - + + + + + + + + - + @@ -174,6 +162,6 @@ exports[`NoDataSource should render normally 1`] = ` - + `; diff --git a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx index 4fc257e5355a..7dec36bda609 100644 --- a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx @@ -28,7 +28,7 @@ describe('NoDataSource', () => { await nextTick(); - const button = await container.findByTestId('dataSourceEmptyStateHeaderButton'); + const button = await container.findByTestId('dataSourceEmptyMenuHeaderLink'); button.click(); expect(container.getByTestId('dataSourceEmptyStatePopover')).toBeVisible(); @@ -44,7 +44,7 @@ describe('NoDataSource', () => { await nextTick(); - const button = await container.findByTestId('dataSourceEmptyStateHeaderButton'); + const button = await container.findByTestId('dataSourceEmptyMenuHeaderLink'); button.click(); const redirectButton = await container.findByTestId( 'dataSourceEmptyStateManageDataSourceButton' diff --git a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx index 7d2142a31765..d10efe8c4a7b 100644 --- a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx +++ b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx @@ -4,55 +4,44 @@ */ import React, { useState } from 'react'; +import { i18n } from '@osd/i18n'; import { EuiButton, - EuiButtonEmpty, EuiPanel, EuiPopover, EuiText, - EuiPopoverFooter, EuiFlexGroup, EuiFlexItem, + EuiButtonIcon, } from '@elastic/eui'; import { ApplicationStart } from 'opensearch-dashboards/public'; import { FormattedMessage } from 'react-intl'; import { DataSourceDropDownHeader } from '../drop_down_header'; import { DSM_APP_ID } from '../../plugin'; +import { EmptyIcon } from '../custom_database_icon'; interface DataSourceDropDownHeaderProps { - totalDataSourceCount: number; - activeDataSourceCount?: number; application?: ApplicationStart; } -export const NoDataSource: React.FC = ({ - activeDataSourceCount, - totalDataSourceCount, - application, -}) => { +export const NoDataSource: React.FC = ({ application }) => { const [showPopover, setShowPopover] = useState(false); - const label = ' No data sources'; const button = ( - } size="s" - color="primary" - onClick={() => { - setShowPopover(!showPopover); - }} - > - {label} - + onClick={() => setShowPopover(!showPopover)} + /> ); const redirectButton = ( = ({ { } @@ -82,8 +71,8 @@ export const NoDataSource: React.FC = ({ { } @@ -92,7 +81,6 @@ export const NoDataSource: React.FC = ({ return ( = ({ anchorPosition="downLeft" data-test-subj={'dataSourceEmptyStatePopover'} > - + - - {text} - - - - - {redirectButton} + + {text} - + + {redirectButton} + + ); }; diff --git a/src/plugins/data_source_management/public/components/toast_button/index.ts b/src/plugins/data_source_management/public/components/toast_button/index.ts index fd881fc3d882..eb1974ff2bd5 100644 --- a/src/plugins/data_source_management/public/components/toast_button/index.ts +++ b/src/plugins/data_source_management/public/components/toast_button/index.ts @@ -3,3 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ export { getReloadButton } from './reload_button'; +export { getManageDataSourceButton } from './manage_data_source_button'; diff --git a/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx new file mode 100644 index 000000000000..6222f74fdec4 --- /dev/null +++ b/src/plugins/data_source_management/public/components/toast_button/manage_data_source_button.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { ApplicationStart } from 'opensearch-dashboards/public'; +import { DSM_APP_ID } from '../../plugin'; + +export const getManageDataSourceButton = (application?: ApplicationStart) => { + return ( + <> + + + + application?.navigateToApp('management', { + path: `opensearch-dashboards/${DSM_APP_ID}`, + }) + } + > + {i18n.translate('dataSourceMenu.manageDataSourceToastButtonLabel', { + defaultMessage: 'Manage data sources', + })} + + + + + ); +}; diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index 3a9443b9183f..b2628e3d3062 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -87,8 +87,9 @@ describe('DataSourceManagement: Utils.ts', () => { const { toasts } = notificationServiceMock.createStartContract(); test('should send warning when data source is not available', () => { - handleNoAvailableDataSourceError(toasts); - expect(toasts.addWarning).toHaveBeenCalledWith(`Data source is not available`); + const changeState = jest.fn(); + handleNoAvailableDataSourceError(changeState, toasts); + expect(toasts.add).toBeCalledTimes(1); }); }); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 9d04f6b7edf0..8f635f840aec 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -24,7 +24,7 @@ import { DataSourceOption } from './data_source_menu/types'; import { DataSourceGroupLabelOption } from './data_source_menu/types'; import { createGetterSetter } from '../../../opensearch_dashboards_utils/public'; import { toMountPoint } from '../../../opensearch_dashboards_react/public'; -import { getReloadButton } from './toast_button'; +import { getManageDataSourceButton, getReloadButton } from './toast_button'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -87,12 +87,21 @@ export async function setFirstDataSourceAsDefault( } } -export function handleNoAvailableDataSourceError(notifications: ToastsStart) { - notifications.addWarning( - i18n.translate('dataSource.noAvailableDataSourceError', { - defaultMessage: `Data source is not available`, - }) - ); +export function handleNoAvailableDataSourceError( + changeState: () => void, + notifications: ToastsStart, + application?: ApplicationStart, + callback?: (ds: DataSourceOption[]) => void +) { + changeState(); + if (callback) callback([]); + notifications.add({ + title: i18n.translate('dataSource.noAvailableDataSourceError', { + defaultMessage: 'No data sources connected yet. Connect your data sources to get started.', + }), + text: toMountPoint(getManageDataSourceButton(application)), + color: 'warning', + }); } export function getFilteredDataSources(