Skip to content

Commit

Permalink
[Multiple Datasource] Add data source aggregated view to show all com…
Browse files Browse the repository at this point in the history
…patible data sources or only show used data sources (#6129) (#6264)

* add data source aggregated view to show all compatible data sources or only used data sources

Signed-off-by: Lu Yu <nluyu@amazon.com>

* add change log

Signed-off-by: Lu Yu <nluyu@amazon.com>

* address comments and add more tests

Signed-off-by: Lu Yu <nluyu@amazon.com>

---------

Signed-off-by: Lu Yu <nluyu@amazon.com>
(cherry picked from commit 05abf5e)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 1f42a6c commit 2971dec
Show file tree
Hide file tree
Showing 9 changed files with 864 additions and 9 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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';

describe('DataSourceAggregatedView', () => {
let component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
let client: SavedObjectsClientContract;
const { toasts } = notificationServiceMock.createStartContract();

beforeEach(() => {
client = {
find: jest.fn().mockResolvedValue([]),
} as any;
mockResponseForSavedObjectsCalls(client, 'find', getDataSourcesWithFieldsResponse);
});

it('should render normally with local cluster not hidden and all options', () => {
component = shallow(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={false}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={true}
/>
);
expect(component).toMatchSnapshot();
expect(client.find).toBeCalledWith({
fields: ['id', 'title', 'auth.type'],
perPage: 10000,
type: 'data-source',
});
expect(toasts.addWarning).toBeCalledTimes(0);
});

it('should render normally with local cluster hidden and all options', () => {
component = shallow(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={true}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={true}
/>
);
expect(component).toMatchSnapshot();
expect(client.find).toBeCalledWith({
fields: ['id', 'title', 'auth.type'],
perPage: 10000,
type: 'data-source',
});
expect(toasts.addWarning).toBeCalledTimes(0);
});

it('should render normally with local cluster and actice selections', () => {
component = shallow(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={false}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={false}
activeDataSourceIds={['test1']}
/>
);
expect(component).toMatchSnapshot();
expect(client.find).toBeCalledWith({
fields: ['id', 'title', 'auth.type'],
perPage: 10000,
type: 'data-source',
});
expect(toasts.addWarning).toBeCalledTimes(0);
});

it('should render normally with data source filter', () => {
component = shallow(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={false}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={false}
dataSourceFilter={(ds) => ds.attributes.auth.type !== 'no_auth'}
/>
);
expect(component).toMatchSnapshot();
expect(client.find).toBeCalledWith({
fields: ['id', 'title', 'auth.type'],
perPage: 10000,
type: 'data-source',
});
expect(toasts.addWarning).toBeCalledTimes(0);
});

it('should render popup when clicking on info icon', async () => {
const container = render(
<DataSourceAggregatedView
fullWidth={false}
hideLocalCluster={false}
savedObjectsClient={client}
notifications={toasts}
displayAllCompatibleDataSources={false}
activeDataSourceIds={['test1']}
/>
);
const infoIcon = await container.findByTestId('dataSourceAggregatedViewInfoButton');
infoIcon.click();
expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiContextMenu,
EuiNotificationBadge,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public';
import { getDataSourcesWithFields } from '../utils';
import { SavedObject } from '../../../../../core/public';
import { DataSourceAttributes } from '../../types';

interface DataSourceAggregatedViewProps {
savedObjectsClient: SavedObjectsClientContract;
notifications: ToastsStart;
hideLocalCluster: boolean;
fullWidth: boolean;
activeDataSourceIds?: string[];
dataSourceFilter?: (dataSource: SavedObject<DataSourceAttributes>) => boolean;
displayAllCompatibleDataSources: boolean;
}

interface DataSourceAggregatedViewState {
isPopoverOpen: boolean;
allDataSourcesIdToTitleMap: Map<string, any>;
}

export class DataSourceAggregatedView extends React.Component<
DataSourceAggregatedViewProps,
DataSourceAggregatedViewState
> {
private _isMounted: boolean = false;

constructor(props: DataSourceAggregatedViewProps) {
super(props);

this.state = {
isPopoverOpen: false,
allDataSourcesIdToTitleMap: new Map(),
};
}

componentWillUnmount() {
this._isMounted = false;
}

onClick() {
this.setState({ ...this.state, isPopoverOpen: !this.state.isPopoverOpen });
}

closePopover() {
this.setState({ ...this.state, isPopoverOpen: false });
}

async componentDidMount() {
this._isMounted = true;
getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type'])
.then((fetchedDataSources) => {
if (fetchedDataSources?.length) {
let filteredDataSources = fetchedDataSources;
if (this.props.dataSourceFilter) {
filteredDataSources = fetchedDataSources.filter((ds) =>
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,
});
}
})
.catch(() => {
this.props.notifications.addWarning(
i18n.translate('dataSource.fetchDataSourceError', {
defaultMessage: 'Unable to fetch existing data sources',
})
);
});
}

render() {
const button = (
<EuiButtonIcon
data-test-subj="dataSourceAggregatedViewInfoButton"
iconType="iInCircle"
display="empty"
aria-label="show data sources"
onClick={this.onClick.bind(this)}
/>
);

let items = [];

// only display active data sources
if (this.props.activeDataSourceIds && this.props.activeDataSourceIds.length > 0) {
items = this.props.activeDataSourceIds.map((id) => {
return {
name: this.state.allDataSourcesIdToTitleMap.get(id),
disabled: true,
};
});
} else {
items = [...this.state.allDataSourcesIdToTitleMap.values()].map((title) => {
return {
name: title,
disabled: true,
};
});
}

const title = this.props.displayAllCompatibleDataSources
? `Data sources (${this.state.allDataSourcesIdToTitleMap.size})`
: 'Selected data sources';

const panels = [
{
id: 0,
title,
items,
},
];

return (
<>
<EuiButtonEmpty
className="euiHeaderLink"
data-test-subj="dataSourceAggregatedViewContextMenuHeaderLink"
aria-label={i18n.translate('dataSourceAggregatedView.dataSourceOptionsButtonAriaLabel', {
defaultMessage: 'dataSourceAggregatedViewMenuButton',
})}
iconType="database"
iconSide="left"
size="s"
disabled={true}
>
{'Data sources'}
</EuiButtonEmpty>
<EuiNotificationBadge color={'subdued'}>
{this.props.activeDataSourceIds?.length || 'All'}
</EuiNotificationBadge>
<EuiPopover
id={'dataSourceSViewContextMenuPopover'}
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { DataSourceAggregatedView } from './data_source_aggregated_view';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from '../../../../../core/public';
import { notificationServiceMock } from '../../../../../core/public/mocks';
import React from 'react';
import { DataSourceMenu } from './data_source_menu';
import { render } from '@testing-library/react';

describe('DataSourceMenu', () => {
let component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
Expand Down Expand Up @@ -66,4 +67,18 @@ describe('DataSourceMenu', () => {
);
expect(component).toMatchSnapshot();
});

it('should render data source aggregated view', () => {
const container = render(
<DataSourceMenu
showDataSourceAggregatedView={true}
appName={'myapp'}
fullWidth={true}
className={'myclass'}
savedObjects={client}
notifications={notifications}
/>
);
expect(container).toMatchSnapshot();
});
});
Loading

0 comments on commit 2971dec

Please sign in to comment.