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.scss b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.scss index 0c10d76c6bd8..62f6239424aa 100644 --- 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 @@ -11,10 +11,20 @@ .euiSelectableListItem__content { cursor: default; + + + .euiSelectableListItem__text { + max-height: 100%; + } } - /* stylelint-enable */ .dataSourceAggregatedViewOuiFlexGroup { + .euiFlexItem { + max-width: fit-content; + } + + /* stylelint-enable */ + .dataSourceAggregatedViewOuiFlexItem { color: grey; text-overflow: ellipsis; 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.scss b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.scss index 4bc7296e3647..fc0e2775bc43 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.scss +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.scss @@ -1,12 +1,23 @@ .dataSourceSelectableOuiPanel { width: 300px; + /* stylelint-disable */ + .euiSelectableListItem__text { + max-height: 100%; + } + .dataSourceSelectableOuiFlexGroup { + .euiFlexItem { + max-width: fit-content; + } + /* stylelint-enable */ + .dataSourceSelectableOuiFlexItem { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; display: inline-block; + max-width: fit-content; } } } 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 f419ed7e6868..9aae5583cab9 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.scss b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.scss index 03235cd38537..3a91bd6bbe7d 100644 --- a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.scss +++ b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.scss @@ -1,8 +1,19 @@ .dataSourceViewOuiPanel { width: 300px; + + /* stylelint-disable */ + + .euiSelectableListItem__text { + max-height: 100%; + } } .dataSourceViewOuiFlexGroup { + .euiFlexItem { + max-width: fit-content; + } + /* stylelint-enable */ + .dataSourceViewOuiFlexItem { color: grey; text-overflow: ellipsis; 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 6e1d135dcb3d..dd032f329753 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 @@ -27,7 +27,6 @@ import { 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'; import { DataSourceViewError } from './data_source_view_error'; @@ -58,7 +57,7 @@ export class DataSourceView extends React.Component - ); - } if (this.state.showError) { return ( - 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(