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 (opensearch-project#6327)

Signed-off-by: Yuanqi(Ella) Zhu <[email protected]>
zhyuanqi authored Apr 4, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent fc3fef2 commit 726fb0e
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
@@ -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))
- [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303))

Original file line number Diff line number Diff line change
@@ -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>
);
Original file line number Diff line number Diff line change
@@ -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;
@@ -75,6 +75,7 @@ export function DataSourceMenu<T>(props: DataSourceMenuProps<T>): ReactElement |
dataSourceFilter={dataSourceFilter}
hideLocalCluster={hideLocalCluster || false}
fullWidth={fullWidth}
uiSettings={uiSettings}
/>
);
}
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
NotificationsStart,
SavedObjectsClientContract,
SavedObject,
IUiSettingsClient,
} from '../../../../../core/public';
import { DataSourceAttributes } from '../../types';

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

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
@@ -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>>;
@@ -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}
@@ -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',
},
],
});
@@ -151,6 +154,7 @@ describe('DataSourceSelectable', () => {
label: 'test2',
},
],
defaultDataSource: null,
isPopoverOpen: false,
selectedOption: [
{
@@ -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
@@ -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';
@@ -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<
@@ -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);
@@ -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(
i18n.translate('dataSource.fetchDataSourceError', {
defaultMessage: 'Unable to fetch existing data sources',
})
);
}
}

onChange(options: SelectedDataSourceOption[]) {
@@ -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"
@@ -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">
<EuiFlexItem grow={1}>{option.label}</EuiFlexItem>
{option.id === this.state.defaultDataSource && (
<EuiFlexItem grow={false}>
<EuiBadge iconSide="left">Default</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
>
{(list, search) => (
<>
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import {
noAuthCredentialAuthMethod,
} from '../types';
import { AuthenticationMethodRegistry } from '../auth_registry';
import { DataSourceOption } from './data_source_selector/data_source_selector';
import { DataSourceOption } from './data_source_menu/types';

export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) {
return savedObjectsClient
4 changes: 4 additions & 0 deletions src/plugins/data_source_management/public/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -21,5 +21,9 @@ describe('#dataSourceManagement', () => {
const start = doStart();
const registry = start.getAuthenticationMethodRegistry();
expect(registry.getAuthenticationMethod('typeA')).toEqual(typeA);
expect(setup.ui).toEqual({
DataSourceSelector: expect.any(Function),
getDataSourceMenu: expect.any(Function),
});
});
});
2 changes: 1 addition & 1 deletion src/plugins/data_source_management/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -104,7 +104,7 @@ export class DataSourceManagementPlugin
registerAuthenticationMethod,
ui: {
DataSourceSelector: createDataSourceSelector(uiSettings),
getDataSourceMenu: <T>() => createDataSourceMenu<T>(),
getDataSourceMenu: <T>() => createDataSourceMenu<T>(uiSettings),
},
};
}

0 comments on commit 726fb0e

Please sign in to comment.