Skip to content

Commit

Permalink
[MDS] Modify toast + popover warning to include incompatible datasour…
Browse files Browse the repository at this point in the history
…ces (opensearch-project#6678)

* Fix merge conflict

Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com>

* Refactor to incompatibleDataSourcesExist

Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com>

* Changeset file for PR opensearch-project#6678 created/updated

* Move required args to the top

Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com>

---------

Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Signed-off-by: yujin-emma <yujin.emma.work@gmail.com>
  • Loading branch information
2 people authored and yujin-emma committed May 16, 2024
1 parent e4b74a1 commit 49ae21f
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 73 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/6678.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- [MDS] Add a new message to data source components when there are no compatible datasources ([#6678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6678))
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ export const LocalCluster: DataSourceOption = {
}),
id: '',
};

export const NO_DATASOURCES_CONNECTED_MESSAGE = 'No data sources connected yet.';
export const CONNECT_DATASOURCES_MESSAGE = 'Connect your data sources to get started.';
export const NO_COMPATIBLE_DATASOURCES_MESSAGE = 'No compatible data sources are available.';
export const ADD_COMPATIBLE_DATASOURCES_MESSAGE = 'Add a compatible data source.';
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import { ShallowWrapper, shallow } from 'enzyme';
import React from 'react';
import { i18n } from '@osd/i18n';
import { DataSourceAggregatedView } from './data_source_aggregated_view';
import { SavedObject, SavedObjectsClientContract } from '../../../../../core/public';
import { IToasts, SavedObject, SavedObjectsClientContract } from '../../../../../core/public';
import {
applicationServiceMock,
notificationServiceMock,
Expand All @@ -21,6 +22,12 @@ import {
import * as utils from '../utils';
import { EuiSelectable, EuiSwitch } from '@elastic/eui';
import { DataSourceAttributes } from '../../types';
import {
ADD_COMPATIBLE_DATASOURCES_MESSAGE,
CONNECT_DATASOURCES_MESSAGE,
NO_COMPATIBLE_DATASOURCES_MESSAGE,
NO_DATASOURCES_CONNECTED_MESSAGE,
} from '../constants';

describe('DataSourceAggregatedView: read all view (displayAllCompatibleDataSources is set to true)', () => {
let component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
Expand Down Expand Up @@ -409,14 +416,15 @@ describe('DataSourceAggregatedView empty state test due to filter out with local
dataSourceFilter={filter}
/>
);
const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`;

expect(component).toMatchSnapshot();
await nextTick();
expect(toasts.add).toHaveBeenCalledTimes(1);
expect(toasts.add.mock.calls[0][0]).toEqual({
color: 'warning',
text: expect.any(Function),
title: 'No data sources connected yet. Connect your data sources to get started.',
title: noCompatibleDataSourcesMessage,
});
expect(component.state('showEmptyState')).toBe(true);
await nextTick();
Expand Down Expand Up @@ -502,3 +510,64 @@ describe('DataSourceAggregatedView error state test no matter hide local cluster
}
);
});

describe('DataSourceAggregatedView warning messages', () => {
const client = {} as any;
const uiSettings = uiSettingsServiceMock.createStartContract();
const nextTick = () => new Promise((res) => process.nextTick(res));
let toasts: IToasts;
const noDataSourcesConnectedMessage = `${NO_DATASOURCES_CONNECTED_MESSAGE} ${CONNECT_DATASOURCES_MESSAGE}`;
const noCompatibleDataSourcesMessage = `${NO_COMPATIBLE_DATASOURCES_MESSAGE} ${ADD_COMPATIBLE_DATASOURCES_MESSAGE}`;

beforeEach(() => {
toasts = notificationServiceMock.createStartContract().toasts;
mockUiSettingsCalls(uiSettings, 'get', 'test1');
});

it.each([
{
findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse),
defaultMessage: noCompatibleDataSourcesMessage,
activeDataSourceIds: ['test2'],
},
{
findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }),
defaultMessage: noDataSourcesConnectedMessage,
activeDataSourceIds: ['test2'],
},
{
findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse),
defaultMessage: noCompatibleDataSourcesMessage,
activeDataSourceIds: undefined,
},
{
findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }),
defaultMessage: noDataSourcesConnectedMessage,
activeDataSourceIds: undefined,
},
])(
'should display correct warning message when no datasource selections are available and local cluster is hidden',
async ({ findFunc, defaultMessage, activeDataSourceIds }) => {
client.find = findFunc;
shallow(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={true}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={!!!activeDataSourceIds}
activeDataSourceIds={activeDataSourceIds}
dataSourceFilter={(_) => false}
uiSettings={uiSettings}
/>
);
await nextTick();

expect(toasts.add).toBeCalledWith(
expect.objectContaining({
title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }),
})
);
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface DataSourceAggregatedViewState extends DataSourceBaseState {
allDataSourcesIdToTitleMap: Map<string, any>;
switchChecked: boolean;
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
}

interface DataSourceOptionDisplay extends DataSourceOption {
Expand All @@ -68,6 +69,7 @@ export class DataSourceAggregatedView extends React.Component<
showError: false,
switchChecked: false,
defaultDataSource: null,
incompatibleDataSourcesExist: false,
};
}

Expand Down Expand Up @@ -113,11 +115,12 @@ export class DataSourceAggregatedView extends React.Component<
}

if (allDataSourcesIdToTitleMap.size === 0) {
handleNoAvailableDataSourceError(
this.onEmptyState.bind(this),
this.props.notifications,
this.props.application
);
handleNoAvailableDataSourceError({
changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length),
notifications: this.props.notifications,
application: this.props.application,
incompatibleDataSourcesExist: !!fetchedDataSources?.length,
});
return;
}

Expand All @@ -133,8 +136,8 @@ export class DataSourceAggregatedView extends React.Component<
});
}

onEmptyState() {
this.setState({ showEmptyState: true });
onEmptyState(incompatibleDataSourcesExist: boolean) {
this.setState({ showEmptyState: true, incompatibleDataSourcesExist });
}

onError() {
Expand All @@ -143,7 +146,12 @@ export class DataSourceAggregatedView extends React.Component<

render() {
if (this.state.showEmptyState) {
return <NoDataSource application={this.props.application} />;
return (
<NoDataSource
application={this.props.application}
incompatibleDataSourcesExist={this.state.incompatibleDataSourcesExist}
/>
);
}
if (this.state.showError) {
return <DataSourceErrorMenu application={this.props.application} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface DataSourceMultiSeletableState extends DataSourceBaseState {
dataSourceOptions: SelectedDataSourceOption[];
selectedOptions: SelectedDataSourceOption[];
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
}

export class DataSourceMultiSelectable extends React.Component<
Expand All @@ -51,6 +52,7 @@ export class DataSourceMultiSelectable extends React.Component<
defaultDataSource: null,
showEmptyState: false,
showError: false,
incompatibleDataSourcesExist: false,
};
}

Expand Down Expand Up @@ -90,12 +92,13 @@ 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
);
handleNoAvailableDataSourceError({
changeState: this.onEmptyState.bind(this, !!fetchedDataSources?.length),
notifications: this.props.notifications,
application: this.props.application,
callback: this.props.onSelectedDataSources,
incompatibleDataSourcesExist: !!fetchedDataSources?.length,
});
return;
}

Expand All @@ -115,8 +118,8 @@ export class DataSourceMultiSelectable extends React.Component<
}
}

onEmptyState() {
this.setState({ showEmptyState: true });
onEmptyState(incompatibleDataSourcesExist: boolean) {
this.setState({ showEmptyState: true, incompatibleDataSourcesExist });
}

onError() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { ShallowWrapper, shallow, mount } from 'enzyme';
import { i18n } from '@osd/i18n';
import { SavedObjectsClientContract } from '../../../../../core/public';
import { notificationServiceMock } from '../../../../../core/public/mocks';
import React from 'react';
Expand All @@ -12,13 +13,21 @@ import { AuthType } from '../../types';
import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks';
import { render } from '@testing-library/react';
import * as utils from '../utils';
import {
NO_DATASOURCES_CONNECTED_MESSAGE,
CONNECT_DATASOURCES_MESSAGE,
NO_COMPATIBLE_DATASOURCES_MESSAGE,
ADD_COMPATIBLE_DATASOURCES_MESSAGE,
} from '../constants';

describe('DataSourceSelectable', () => {
let component: ShallowWrapper<any, Readonly<{}>, 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}`;

beforeEach(() => {
client = {
Expand Down Expand Up @@ -145,6 +154,7 @@ describe('DataSourceSelectable', () => {
},
],
showError: false,
incompatibleDataSourcesExist: false,
});

containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]);
Expand All @@ -167,6 +177,7 @@ describe('DataSourceSelectable', () => {
},
],
showError: false,
incompatibleDataSourcesExist: false,
});

expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]);
Expand Down Expand Up @@ -345,6 +356,7 @@ describe('DataSourceSelectable', () => {
},
],
showError: false,
incompatibleDataSourcesExist: false,
});
});

Expand Down Expand Up @@ -374,6 +386,7 @@ describe('DataSourceSelectable', () => {
selectedOption: [],
showEmptyState: false,
showError: true,
incompatibleDataSourcesExist: false,
});

containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]);
Expand All @@ -396,27 +409,59 @@ describe('DataSourceSelectable', () => {
},
],
showError: true,
incompatibleDataSourcesExist: false,
});

expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]);
expect(onSelectedDataSource).toHaveBeenCalled();
});
it('should render no data source when no data source filtered out and hide local cluster', async () => {
const onSelectedDataSource = jest.fn();
render(
<DataSourceSelectable
savedObjectsClient={client}
notifications={toasts}
onSelectedDataSources={onSelectedDataSource}
disabled={false}
hideLocalCluster={true}
fullWidth={false}
selectedOption={[{ id: 'test2' }]}
dataSourceFilter={(ds) => false}
/>
);
await nextTick();
expect(toasts.add).toBeCalled();
expect(onSelectedDataSource).toBeCalledWith([]);
});

it.each([
{
findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }),
defaultMessage: noDataSourcesConnectedMessage,
selectedOption: undefined,
},
{
findFunc: jest.fn().mockResolvedValue({ savedObjects: [] }),
defaultMessage: noDataSourcesConnectedMessage,
selectedOption: [{ id: 'test2' }],
},
{
findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse),
defaultMessage: noCompatibleDataSourcesMessage,
selectedOption: undefined,
},
{
findFunc: jest.fn().mockResolvedValue(getDataSourcesWithFieldsResponse),
defaultMessage: noCompatibleDataSourcesMessage,
selectedOption: [{ id: 'test2' }],
},
])(
'should render correct message when there are no datasource options available and local cluster is hidden',
async ({ findFunc, selectedOption, defaultMessage }) => {
client.find = findFunc;
const onSelectedDataSource = jest.fn();
render(
<DataSourceSelectable
savedObjectsClient={client}
notifications={toasts}
onSelectedDataSources={onSelectedDataSource}
disabled={false}
hideLocalCluster={true}
fullWidth={false}
selectedOption={selectedOption}
dataSourceFilter={(ds) => false}
/>
);
await nextTick();

expect(toasts.add).toBeCalledWith(
expect.objectContaining({
title: i18n.translate('dataSource.noAvailableDataSourceError', { defaultMessage }),
})
);
expect(onSelectedDataSource).toBeCalledWith([]);
}
);
});
Loading

0 comments on commit 49ae21f

Please sign in to comment.