From c4af2ab583158c634ec73ad5aa1aa795b572ffa8 Mon Sep 17 00:00:00 2001 From: Yu Jin <112784385+yujin-emma@users.noreply.github.com> Date: Tue, 16 Apr 2024 23:48:32 -0700 Subject: [PATCH 1/2] add empty state and test except the aggregated view (#6499) * add empty state and test except the aggregated view Signed-off-by: yujin-emma * update change log Signed-off-by: yujin-emma * adjust the order for filter options and empty state check Signed-off-by: yujin-emma * fix lint Signed-off-by: yujin-emma --------- Signed-off-by: yujin-emma Signed-off-by: Yu Jin <112784385+yujin-emma@users.noreply.github.com> --- CHANGELOG.md | 1 + .../data_source_aggregated_view.tsx | 6 ++++- .../data_source_menu/data_source_menu.tsx | 1 + .../components/data_source_menu/types.ts | 1 + .../data_source_multi_selectable.tsx | 6 +++++ .../data_source_selectable.test.tsx | 5 ++++ .../data_source_selectable.tsx | 10 ++++++++ .../data_source_view/data_source_view.tsx | 5 ++++ .../no_data_source.test.tsx.snap | 14 +++++++++++ .../components/no_data_source/index.tsx | 6 +++++ .../no_data_source/no_data_source.test.tsx | 16 +++++++++++++ .../no_data_source/no_data_source.tsx | 24 +++++++++++++++++++ 12 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/no_data_source/index.tsx create mode 100644 src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx create mode 100644 src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d1fad31e9c..48c2d6561e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Mulitple Datasource] Add multi data source support to TSVB ([#6298](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6298)) - [Multiple Datasource] Add installedPlugins list to data source saved object ([#6348](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6348)) - [Multiple Datasource] Add default icon in multi-selectable picker ([#6357](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6357)) +- [Multiple Datasource] Add empty state component for no connected data source ([#6499](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6499)) - [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) - [Workspace] Filter left nav menu items according to the current workspace ([#6234](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6234)) - [Multiple Datasource] Add multi data source support to Timeline ([#6385](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6385)) diff --git a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx index dda3da78363b..c77a49a75831 100644 --- a/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx +++ b/src/plugins/data_source_management/public/components/data_source_aggregated_view/data_source_aggregated_view.tsx @@ -16,6 +16,7 @@ import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/p import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; +import { NoDataSource } from '../no_data_source'; import { DataSourceErrorMenu } from '../data_source_error_menu'; import { DataSourceBaseState } from '../data_source_menu/types'; @@ -46,6 +47,7 @@ export class DataSourceAggregatedView extends React.Component< this.state = { isPopoverOpen: false, allDataSourcesIdToTitleMap: new Map(), + showEmptyState: false, showError: false, }; } @@ -73,7 +75,6 @@ export class DataSourceAggregatedView extends React.Component< this.props.dataSourceFilter!(ds) ); } - const allDataSourcesIdToTitleMap = new Map(); filteredDataSources.forEach((ds) => { @@ -101,6 +102,9 @@ export class DataSourceAggregatedView extends React.Component< } render() { + if (this.state.showEmptyState) { + return ; + } if (this.state.showError) { return ; } diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx index bc645a0b885f..9c011a2e8e47 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx @@ -20,6 +20,7 @@ import { DataSourceSelectable } from '../data_source_selectable'; export function DataSourceMenu(props: DataSourceMenuProps): ReactElement | null { const { componentType, componentConfig, uiSettings, hideLocalCluster, application } = props; + function renderDataSourceView(config: DataSourceViewConfig): ReactElement | null { const { activeOption, diff --git a/src/plugins/data_source_management/public/components/data_source_menu/types.ts b/src/plugins/data_source_management/public/components/data_source_menu/types.ts index 483f08c524bc..d6d960c0e40a 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/types.ts +++ b/src/plugins/data_source_management/public/components/data_source_menu/types.ts @@ -29,6 +29,7 @@ export interface DataSourceBaseConfig { } export interface DataSourceBaseState { + showEmptyState: boolean; showError: boolean; } diff --git a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx index 25122a801e84..8e842f70b4f5 100644 --- a/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx +++ b/src/plugins/data_source_management/public/components/data_source_multi_selectable/data_source_multi_selectable.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; import { IUiSettingsClient } from 'src/core/public'; import { DataSourceFilterGroup, SelectedDataSourceOption } from './data_source_filter_group'; +import { NoDataSource } from '../no_data_source'; import { getDataSourcesWithFields, handleDataSourceFetchError } from '../utils'; import { DataSourceBaseState } from '../data_source_menu/types'; import { DataSourceErrorMenu } from '../data_source_error_menu'; @@ -39,6 +40,7 @@ export class DataSourceMultiSelectable extends React.Component< dataSourceOptions: [], selectedOptions: [], defaultDataSource: null, + showEmptyState: false, showError: false, }; } @@ -82,6 +84,7 @@ export class DataSourceMultiSelectable extends React.Component< ...this.state, selectedOptions, defaultDataSource, + showEmptyState: (fetchedDataSources?.length === 0 && this.props.hideLocalCluster) || false, }); this.props.onSelectedDataSources(selectedOptions); @@ -107,6 +110,9 @@ export class DataSourceMultiSelectable extends React.Component< } render() { + if (this.state.showEmptyState) { + return ; + } if (this.state.showError) { return ; } diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx index c836dd011085..1ced7e3872c1 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx @@ -139,6 +139,7 @@ describe('DataSourceSelectable', () => { ], defaultDataSource: null, isPopoverOpen: false, + showEmptyState: false, selectedOption: [ { id: 'test2', @@ -159,6 +160,7 @@ describe('DataSourceSelectable', () => { ], defaultDataSource: null, isPopoverOpen: false, + showEmptyState: false, selectedOption: [ { checked: 'on', @@ -337,6 +339,7 @@ describe('DataSourceSelectable', () => { ], defaultDataSource: null, isPopoverOpen: false, + showEmptyState: false, selectedOption: [ { id: 'test2', @@ -371,6 +374,7 @@ describe('DataSourceSelectable', () => { defaultDataSource: null, isPopoverOpen: false, selectedOption: [], + showEmptyState: false, showError: true, }); @@ -385,6 +389,7 @@ describe('DataSourceSelectable', () => { ], defaultDataSource: null, isPopoverOpen: false, + showEmptyState: false, selectedOption: [ { checked: 'on', diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx index 4e2278c280b6..ec4af533c5df 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx @@ -37,6 +37,8 @@ import { } from '../data_source_menu/types'; import { DataSourceErrorMenu } from '../data_source_error_menu'; import { DataSourceItem } from '../data_source_item'; +import { NoDataSource } from '../no_data_source'; +import './data_source_selectable.scss'; import { DataSourceDropDownHeader } from '../drop_down_header'; import '../button_title.scss'; import './data_source_selectable.scss'; @@ -81,6 +83,7 @@ export class DataSourceSelectable extends React.Component< isPopoverOpen: false, selectedOption: [], defaultDataSource: null, + showEmptyState: false, showError: false, }; @@ -186,6 +189,10 @@ export class DataSourceSelectable extends React.Component< this.props.dataSourceFilter ); + if (dataSourceOptions.length === 0 && this.props.hideLocalCluster) { + this.setState({ showEmptyState: true }); + } + if (!this.props.hideLocalCluster) { dataSourceOptions.unshift(LocalCluster); } @@ -243,6 +250,9 @@ export class DataSourceSelectable extends React.Component< }; render() { + if (this.state.showEmptyState) { + return ; + } if (this.state.showError) { return ; } diff --git a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx index 420bb5927145..810413db1f8c 100644 --- a/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx +++ b/src/plugins/data_source_management/public/components/data_source_view/data_source_view.tsx @@ -17,6 +17,7 @@ import { handleNoAvailableDataSourceError, } from '../utils'; import { LocalCluster } from '../constants'; +import { NoDataSource } from '../no_data_source'; interface DataSourceViewProps { fullWidth: boolean; @@ -43,6 +44,7 @@ export class DataSourceView extends React.Component; + } if (this.state.showError) { return ; } diff --git a/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap new file mode 100644 index 000000000000..8c9f1339fed4 --- /dev/null +++ b/src/plugins/data_source_management/public/components/no_data_source/__snapshots__/no_data_source.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoDataSource should render normally 1`] = ` + + No data sources + +`; diff --git a/src/plugins/data_source_management/public/components/no_data_source/index.tsx b/src/plugins/data_source_management/public/components/no_data_source/index.tsx new file mode 100644 index 000000000000..71fcabf9145c --- /dev/null +++ b/src/plugins/data_source_management/public/components/no_data_source/index.tsx @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { NoDataSource } from './no_data_source'; diff --git a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx new file mode 100644 index 000000000000..380c6ad77e4f --- /dev/null +++ b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.test.tsx @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ShallowWrapper, shallow } from 'enzyme'; +import React from 'react'; +import { NoDataSource } from './no_data_source'; + +describe('NoDataSource', () => { + let component: ShallowWrapper, React.Component<{}, {}, any>>; + it('should render normally', () => { + component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx new file mode 100644 index 000000000000..2f9cf8c8415a --- /dev/null +++ b/src/plugins/data_source_management/public/components/no_data_source/no_data_source.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { EuiButtonEmpty } from '@elastic/eui'; + +export const NoDataSource = () => { + const label = ' No data sources'; + return ( + + {label} + + ); +}; From d2d410b91da2198007bfe915c44de35dfac7bbf1 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Wed, 17 Apr 2024 00:47:09 -0700 Subject: [PATCH 2/2] [BUG] Check for duplicate data source id before fetching the data sources in index pattern selector (#6498) * remove duplicate entries Signed-off-by: Lu Yu * add test Signed-off-by: Lu Yu --------- Signed-off-by: Lu Yu --- .../index_pattern_select.test.tsx | 56 +++++++++++++++++++ .../index_pattern_select.tsx | 8 ++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx new file mode 100644 index 000000000000..9186aa361a0e --- /dev/null +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { shallow } from 'enzyme'; +import { SavedObjectsClientContract } from '../../../../../core/public'; +import React from 'react'; +import IndexPatternSelect from './index_pattern_select'; + +describe('IndexPatternSelect', () => { + let client: SavedObjectsClientContract; + const bulkGetMock = jest.fn(); + + const nextTick = () => new Promise((res) => process.nextTick(res)); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue({ + savedObjects: [ + { + references: [{ id: 'testDataSourceId', type: 'data-source' }], + attributes: { title: 'testTitle1' }, + }, + { + references: [{ id: 'testDataSourceId', type: 'data-source' }], + attributes: { title: 'testTitle2' }, + }, + ], + }), + bulkGet: bulkGetMock, + get: jest.fn().mockResolvedValue({ + references: [{ id: 'someId', type: 'data-source' }], + attributes: { title: 'testTitle' }, + }), + } as any; + }); + + it('should render index pattern select', async () => { + const onChangeMock = jest.fn(); + const compInstance = shallow( + + ).instance(); + + bulkGetMock.mockResolvedValue({ savedObjects: [{ attributes: { title: 'test1' } }] }); + compInstance.debouncedFetch(''); + await new Promise((resolve) => setTimeout(resolve, 300)); + await nextTick(); + expect(bulkGetMock).toBeCalledWith([{ id: 'testDataSourceId', type: 'data-source' }]); + }); +}); diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 228c803b0f97..9d2d98591bc9 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -172,9 +172,15 @@ export default class IndexPatternSelect extends Component = []; + const dataSourceIdSet = new Set(); savedObjects.map((indexPatternSavedObject: SimpleSavedObject) => { const dataSourceReference = getDataSourceReference(indexPatternSavedObject.references); - if (dataSourceReference && !this.state.dataSourceIdToTitle.has(dataSourceReference.id)) { + if ( + dataSourceReference && + !this.state.dataSourceIdToTitle.has(dataSourceReference.id) && + !dataSourceIdSet.has(dataSourceReference.id) + ) { + dataSourceIdSet.add(dataSourceReference.id); dataSourcesToFetch.push({ type: 'data-source', id: dataSourceReference.id }); } });