Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MDS] Modify toast + popover warning to include incompatible datasources #6678

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
dataSourceOptions: SelectedDataSourceOption[];
selectedOptions: SelectedDataSourceOption[];
defaultDataSource: string | null;
incompatibleDataSourcesExist: boolean;
}

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

Expand Down Expand Up @@ -90,12 +92,13 @@
if (!this._isMounted) return;

if (selectedOptions.length === 0) {
handleNoAvailableDataSourceError(
this.onEmptyState.bind(this),
this.props.notifications,
this.props.application,
this.props.onSelectedDataSources
);
handleNoAvailableDataSourceError({

Check warning on line 95 in src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx#L95

Added line #L95 was not covered by tests
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 @@
}
}

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

Check warning on line 122 in src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx#L122

Added line #L122 was not covered by tests
}

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
Loading