Skip to content

Commit

Permalink
Add default icon for selectable component and make sure the default d…
Browse files Browse the repository at this point in the history
…atasource shows automatically

Signed-off-by: Yuanqi(Ella) Zhu <[email protected]>
  • Loading branch information
zhyuanqi committed Apr 4, 2024
1 parent 557fbcf commit d334720
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218))
- [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315)
- [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052))
- [Multiple Datasource] Add default icon for selectable component and make sure the default datasource shows automatically ([#6327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6327))
- [Multiple Datasource] Pass selected data sources to plugin consumers when the multi-select component initially loads ([#6333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6333))

### 🐛 Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@

import React from 'react';
import { EuiHeaderLinks } from '@elastic/eui';
import { IUiSettingsClient } from 'src/core/public';
import { DataSourceMenu } from './data_source_menu';
import { DataSourceMenuProps } from './types';
import { MountPointPortal } from '../../../../opensearch_dashboards_react/public';

export function createDataSourceMenu<T>() {
export function createDataSourceMenu<T>(uiSettings: IUiSettingsClient) {
return (props: DataSourceMenuProps<T>) => {
if (props.setMenuMountPoint) {
return (
<MountPointPortal setMountPoint={props.setMenuMountPoint}>
<EuiHeaderLinks data-test-subj="top-nav" gutterSize="xs">
<DataSourceMenu {...props} />
<DataSourceMenu {...props} uiSettings={uiSettings} />
</EuiHeaderLinks>
</MountPointPortal>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { DataSourceSelectable } from '../data_source_selectable';

export function DataSourceMenu<T>(props: DataSourceMenuProps<T>): ReactElement | null {
const { componentType, componentConfig } = props;
const { componentType, componentConfig, uiSettings } = props;

function renderDataSourceView(config: DataSourceViewConfig): ReactElement | null {
const { activeOption, fullWidth, savedObjects, notifications } = config;
Expand Down Expand Up @@ -75,6 +75,7 @@ export function DataSourceMenu<T>(props: DataSourceMenuProps<T>): ReactElement |
dataSourceFilter={dataSourceFilter}
hideLocalCluster={hideLocalCluster || false}
fullWidth={fullWidth}
uiSettings={uiSettings}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
NotificationsStart,
SavedObjectsClientContract,
SavedObject,
IUiSettingsClient,
} from '../../../../../core/public';
import { DataSourceAttributes } from '../../types';

Expand All @@ -23,6 +24,7 @@ export interface DataSourceBaseConfig {
export interface DataSourceMenuProps<T = any> {
componentType: DataSourceComponentType;
componentConfig: T;
uiSettings?: IUiSettingsClient;
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
}

Expand Down

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 @@ -11,6 +11,7 @@ import { DataSourceSelectable } from './data_source_selectable';
import { AuthType } from '../../types';
import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks';
import { render } from '@testing-library/react';
import * as utils from '../utils';

describe('DataSourceSelectable', () => {
let component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
Expand Down Expand Up @@ -109,6 +110,7 @@ describe('DataSourceSelectable', () => {

it('should callback if changed state', async () => {
const onSelectedDataSource = jest.fn();
spyOn(utils, 'getDefaultDataSource').and.returnValue([{ id: 'test2', label: 'test2' }]);
const container = mount(
<DataSourceSelectable
savedObjectsClient={client}
Expand All @@ -125,19 +127,20 @@ describe('DataSourceSelectable', () => {
const containerInstance = container.instance();

containerInstance.onChange([{ id: 'test2', label: 'test2' }]);
expect(onSelectedDataSource).toBeCalledTimes(0);
expect(onSelectedDataSource).toBeCalledTimes(1);
expect(containerInstance.state).toEqual({
dataSourceOptions: [
{
id: 'test2',
label: 'test2',
},
],
defaultDataSource: null,
isPopoverOpen: false,
selectedOption: [
{
id: '',
label: 'Local cluster',
id: 'test2',
label: 'test2',
},
],
});
Expand All @@ -151,6 +154,7 @@ describe('DataSourceSelectable', () => {
label: 'test2',
},
],
defaultDataSource: null,
isPopoverOpen: false,
selectedOption: [
{
Expand All @@ -160,7 +164,9 @@ describe('DataSourceSelectable', () => {
},
],
});

expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]);
expect(onSelectedDataSource).toBeCalledTimes(1);
expect(onSelectedDataSource).toHaveBeenCalled();
expect(utils.getDefaultDataSource).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import {
EuiButtonEmpty,
EuiSelectable,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiBadge,
} from '@elastic/eui';
import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public';
import { getDataSourcesWithFields } from '../utils';
import {
IUiSettingsClient,
SavedObjectsClientContract,
ToastsStart,
} from 'opensearch-dashboards/public';
import { getDataSourcesWithFields, getDefaultDataSource } from '../utils';
import { LocalCluster } from '../data_source_selector/data_source_selector';
import { SavedObject } from '../../../../../core/public';
import { DataSourceAttributes } from '../../types';
Expand All @@ -29,16 +36,18 @@ interface DataSourceSelectableProps {
fullWidth: boolean;
selectedOption?: DataSourceOption[];
dataSourceFilter?: (dataSource: SavedObject<DataSourceAttributes>) => boolean;
uiSettings?: IUiSettingsClient;
}

interface DataSourceSelectableState {
dataSourceOptions: SelectedDataSourceOption[];
isPopoverOpen: boolean;
selectedOption?: SelectedDataSourceOption[];
defaultDataSource: string | null;
}

interface SelectedDataSourceOption extends DataSourceOption {
checked?: boolean;
checked?: string;
}

export class DataSourceSelectable extends React.Component<
Expand All @@ -53,11 +62,8 @@ export class DataSourceSelectable extends React.Component<
this.state = {
dataSourceOptions: [],
isPopoverOpen: false,
selectedOption: this.props.selectedOption
? this.props.selectedOption
: this.props.hideLocalCluster
? []
: [LocalCluster],
selectedOption: [],
defaultDataSource: null,
};

this.onChange.bind(this);
Expand All @@ -77,44 +83,72 @@ export class DataSourceSelectable extends React.Component<

async componentDidMount() {
this._isMounted = true;
getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type'])
.then((fetchedDataSources) => {
if (fetchedDataSources?.length) {
let filteredDataSources: Array<SavedObject<DataSourceAttributes>> = [];
if (this.props.dataSourceFilter) {
filteredDataSources = fetchedDataSources.filter((ds) =>
this.props.dataSourceFilter!(ds)
);
}

if (filteredDataSources.length === 0) {
filteredDataSources = fetchedDataSources;
}

const dataSourceOptions = filteredDataSources
.map((dataSource) => ({
id: dataSource.id,
label: dataSource.attributes?.title || '',
}))
.sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
if (!this.props.hideLocalCluster) {
dataSourceOptions.unshift(LocalCluster);
}

if (!this._isMounted) return;
this.setState({
...this.state,
dataSourceOptions,
});
}
})
.catch(() => {
this.props.notifications.addWarning(
i18n.translate('dataSource.fetchDataSourceError', {
defaultMessage: 'Unable to fetch existing data sources',
try {
let filteredDataSources: Array<SavedObject<DataSourceAttributes>> = [];
let dataSourceOptions: DataSourceOption[] = [];

// Fetch data sources with fields
const fetchedDataSources = await getDataSourcesWithFields(this.props.savedObjectsClient, [
'id',
'title',
'auth.type',
]);

if (fetchedDataSources?.length) {
filteredDataSources = this.props.dataSourceFilter
? fetchedDataSources.filter((ds) => this.props.dataSourceFilter!(ds))
: fetchedDataSources;
dataSourceOptions = filteredDataSources
.map((dataSource) => ({
id: dataSource.id,
label: dataSource.attributes?.title || '',
}))
.sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
}

// Add local cluster to the list of data sources if it is not hidden.
if (!this.props.hideLocalCluster) {
dataSourceOptions.unshift(LocalCluster);
}

const defaultDataSource = this.props.uiSettings?.get('defaultDataSource', null) ?? null;
const selectedDataSource = getDefaultDataSource(
filteredDataSources,
LocalCluster,
this.props.uiSettings,
this.props.hideLocalCluster,
this.props.selectedOption
);

if (selectedDataSource.length === 0) {
this.props.notifications.addWarning('No connected data source available.');
} else {
// Update the checked status of the selected data source.
const updatedDataSourceOptions: SelectedDataSourceOption[] = dataSourceOptions.map(
(option) => ({
...option,
...(option.id === selectedDataSource[0].id && { checked: 'on' }),
})
);
});

if (!this._isMounted) return;

this.setState({
...this.state,
dataSourceOptions: updatedDataSourceOptions,
selectedOption: selectedDataSource,
defaultDataSource,
});

this.props.onSelectedDataSources(selectedDataSource);
}
} catch (error) {
this.props.notifications.addWarning(

Check warning on line 146 in src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx#L146

Added line #L146 was not covered by tests
i18n.translate('dataSource.fetchDataSourceError', {
defaultMessage: 'Unable to fetch existing data sources',
})
);
}
}

onChange(options: SelectedDataSourceOption[]) {
Expand Down Expand Up @@ -168,7 +202,7 @@ export class DataSourceSelectable extends React.Component<
data-test-subj={'dataSourceSelectableContextMenuPopover'}
>
<EuiContextMenuPanel>
<EuiPanel color="transparent" paddingSize="s">
<EuiPanel color="transparent" paddingSize="s" style={{ width: '300px' }}>
<EuiSpacer size="s" />
<EuiSelectable
aria-label="Search"
Expand All @@ -180,6 +214,16 @@ export class DataSourceSelectable extends React.Component<
onChange={(newOptions) => this.onChange(newOptions)}
singleSelection={true}
data-test-subj={'dataSourceSelectable'}
renderOption={(option) => (
<EuiFlexGroup alignItems="center">

Check warning on line 218 in src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx#L218

Added line #L218 was not covered by tests
<EuiFlexItem grow={1}>{option.label}</EuiFlexItem>
{option.id === this.state.defaultDataSource && (
<EuiFlexItem grow={false}>
<EuiBadge iconSide="left">Default</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
>
{(list, search) => (
<>
Expand Down
Loading

0 comments on commit d334720

Please sign in to comment.